设为首页 - 加入收藏
广告 1000x90
您的当前位置:黄大仙78345救世报网 > 局部坐标系 > 正文

基于2D SDF的体积字实现

来源:未知 编辑:admin 时间:2019-05-17

  这是侑虎科技第438篇文章,感谢作者燃野供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()

  作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!

  笔者很久之前就想做体积字,并认为在VR中应该是标配,但直到今天也没流行起来,不过倒是看到了Clay Book、Nex Machina这种基于3D SDF体渲染的炫酷游戏。笔者最近为项目加入TextMeshPro,正好基于TMP尝试一下,直观效果见上面的题图。文字形状不是基于字形Mesh的,而是Signed Distance Field的2D图,模型只是Cube,用于提供Pixel Shader的执行来通过Sphere Ray Marching的方式计算屏幕像素是否在体积字内。优点是对显存的占用很低,一个字形只用几K字节,64x64的A8是4K字节,32x32的效果也是可接受的,只计算形状并对正面侧面分别指定颜色的话计算量不大,且主要消耗在文字侧面的计算上,降低文字厚度和调整视角可以减少计算量。复杂的费性能的效果也可以往上加,可以用Triplanar mapping的方式给前后面和侧面加贴图,通过SDF下降梯度能算出侧面法线,整个体积字可以进行光照计算,正面和侧面可以进行圆角的形状和法线过渡,更进一步计算任一像素的切线空间可以加入法线贴图,之后就支持各种常规表面材质了。

  TMP Shader的作者之一(好像是负责写Shader)sschaem在2014年就做过的体积字效果,社区反响不强烈,加之别的功能需求更重要一直没加进去,效果更牛的VRTFX (像是VR Text FX的意思)一直没见发布,参考:

  现在TMP组件里虽然有“Enable Volumetric Setup”,也只是把2D片变成了Cube,仍然没有Shader支持。其实在现有TMP的基础上实现体积字还是挺容易的。本文主要讲述形状计算、字体法线、表面纹理、形状法线和法线纹理,并讨论下相关问题,欢迎大家指正。浏览本文前最好对SDF和raymarching有所了解,比如看这2篇文章:

  文字轮廓用SDF 2D贴图存储,里面每个Texel存的是到最近的文字轮廓的距离,无论是什么方向,轮廓外为负值,轮廓内为正值,运行时对于任意UV通过双线性插值仍然能算出比较精确的距离。在TMP中采用A8格式存储,轮廓是0.5,小于0.5是外侧,从0到0.5对应Padding个像素范围,Padding越大则基于边缘的效果范围就越大,生成字体文件时也就更费计算。

  如上图,TMP自带SDF字体是用1024x1024,每个字形精度是86像素,padding是9,算下来就是每个字符有86+9x2的像素精度。

  2D SDF拉出一个厚度能表示一个3D场,可以对应一个Cube模型,长宽高可以不相等,“显然”于模型内的任一点都能算出是否属于体积字之内。TMP生成的2D片局部坐标都是在XY平面上的,正面朝向负Z,见下图:

  在局部坐标系正Z的方向上加入字符的另外4个点,组成Cube,请记住这个坐标系便于以后理解,XYZ对应红绿蓝。

  体积字的每一个像素点都包含在Cube中,所以从Cube表面每个像素点对应的坐标开始,延视线方向进行RayMarching,步长是跟当前坐标在SDF图里的对应值相关的,SDF里记录了到最近轮廓的距离,但没有方向,如果距离轮廓足够近则认为到达体积字表面,否则采用新的步长继续Marching。

  如果已经March到了Cube Bound外部,就认为是在体积字外部的。判断内外部的时候最好在cube的局部坐标系里,平移后使左下角顶点为原点,其他点都在XYZ的正方向,对于任一点p,p * (bound - p).xyz有小于0的分量就是在外部,需要被Clip掉。一个TMP组件里可能包含多个文字,它们都在一个局部坐标系里,每个字符都得转到字符局部的坐标系计算。

  如果用光了所有步长还不够接近,也认为是到达体积字表面,一般是因为视角近似平行于轮廓表面,造成步长太短,起始已经很接近轮廓了。

  对于位置p需要转到为SDF贴图的UV来采样距离值,假设Cube右上、左下点的坐标和uv分别为posTR, posBL, uvTR, uvBL,只关心xy,不关心z

  在高版本的OpenGL和DirectX中可以为每个顶点指定InstanceId,一个字形Cube的8顶点对应一个实例,字形数据存在Instance相关的buffer中。如果不支持这种特性就得每个顶点都存储字形数据,通过Normal、Tangent、UV通道传到Shader中,本文就是这么做的。

  把顶点转换到字符局部坐标系,存在cuboidLocalPos里,经过光栅化插值后成为RayMarching的起点。

  计算视线方向,PS用插值结果。这里遇到一个问题是想同时支持透视和正交相机,Unity并没有提供这样的函数,UnityCG.cginc里的UnityWorldSpaceViewDir只能算透视viewDir。我觉得应该顶点局部坐标在Clip Space下在面的投影转回局部坐标系做差值算方向,但是发现又没有InvMVP矩阵,现算Inverse矩阵感觉有点费,索性就把2个方向都算出来在用01插值吧,如果有好的方法请您告诉我。

  VS还是比较简单的,一个字符8个顶点,计算量也不大,PS里的重复计算可以移动到VS里来算。

  根据上面提到的算法写RayMarching即可,input.cuboidLocalPos已经是字符局部坐标系下的点了,字符Cube的左下角在局部坐标系的原点。这里对于文字的正反面,在第一次采样SDF后就能Return,不会进行多次for。对于侧面先走步长再Clip,尽量提前剔除掉,对于视角比较接近正面,厚度不大的情况下只用执行少量几次。_outlineEpsilon是用于微调到达轮廓的条件,_outline表示边界的SDF值,默认0.5。最后用执行for循环的次数给文字上灰度色,颜色越深用的循环次数越多。

  调一下_outlineEpsilon立马好很多,黑色部分都是用完for循环也没到达边缘,都是视角跟切线比较接近的情况。

  这里有一个性能相关的问题没想清楚,在PS里执行分支提前Return、Clip,在GPU执行的时候并不是每个像素的PS执行独立的分支,而是一组一起执行,如果condition相同那就不用执行另一个分支了,如果condition不同就得把if else都执行了,甚至空等某个PS的执行。这里的组是2x2的像素,还是GPU实现的Warp什么的?对我写分支该如何取舍,比如是把A、B都算出来用01 lerp进行取舍好,还是用if else好呢?

  要有光照计算的话必须得有法线,正反面法线很简单就是负Z和正Z,侧面法线就是SDF值的下降梯度。

  正面和侧面交界处法线是不连续的,所以光照下有锯齿,那么对法线做一个圆角过渡应该就可以了,用下图做圆角的分析。

  俯视图,OCD是正面,OBA是侧面,O点是交界处,字形轮廓的点都在ox和oz轴上,还没做字形的圆角过渡。现在希望进行半径R的法线过渡。因为已经有了正面和侧面法线,算出一个权重进行过渡比较好,假设要算B点的法线R,R)表示Z方向上距离过渡边界A点是0.5R,轮廓方向上距离过渡边界的D点的距离是R,AD和BE的焦点是G,理论上用DG/AD做侧面法线的权重最好,但是要算各种三角函数,或者泰勒级数展开取前几项,麻烦又费计算。用BED的夹角做权重,看似挺好,其实不对,EA向量和ED向量的线性插值结果的终点都在AD线段上,角度线性变化对应DG长度并不是线性变化的,在两头变化快,在中间变化慢,这是被否定的方法。

  最简单直观,也是我一开是就想到的是用B.y / (B.x + B.y)来做权重,效果如下:

  远看效果还凑合,近看会看到过渡不自然,如下图左侧,是因为B.y / (B.x + B.y)的过渡也不是线性的,同样两边变化快,中间变化慢。比如(0,1)到(0.1,1)变化0.1/1.1≈0.091, (0.9,1)到(1,1)变化0.026。简单的方法是对B求平方之后在算权重,虽然仍然不是线性变化,但是两头变化慢,中间变化快,效果还是很不错的,见下图右侧。

  圆角设置太大会出现瑕疵,一是因为Padding不够大,而是因为2倍半径超过文字笔触宽度。采用大Padding的英文会好一些。

  前后面的表面纹理很简单,跟采样SDF一样,就是多了个tiling&offset。

  6面纹理看起来还算不错,但是在正面和侧面边界有很硬的过渡,因为形状上还没做圆角过渡。

  算出正数表示在外部,在b表示的长方体边界向外扩展,在8个角会扩张为球面,这个公式的意思是说把p中在b外部的分量拿出来求到b边界的距离,大于r算外部。

  对于体积字来说没法向外扩展,只能把边界往里算一些,扩张到当前边界。现在_outline的意思就相当于上面公式的b,并且有模型半径R = _outline * bound.w, bound.w是把SDF值转为模型距离的系数。看下图考虑A、B、C三点对应上面公式中abs(p)-b,是什么

  此时viewDir不用除以xy屏幕投影长度,距离直接当步长。新的raymarching如下:

  法线纹理的数据是在切线空间下的,需要转换到字符局部空间。想了解切线空间和法线可以看这篇文章:

  对于文字正面在局部坐标系中,法线) 也是uv.x的方向, binormal B用正Y方向(0, 1, 0)也是uv.y的方向,切线空间的法线X转换到局部坐标系就是:

  用跟Diffuse一样的方式,用模型的sideNormal混合上下和左右的纹理法线,再跟前后面做混合,得到看似正确的结果,这种混合似乎跟Ground Truth还有差距,目前先这样。效果如下:

  1、抗锯齿:由于MSAA是基于光栅化的,三角形边界并不是文字形状的边界,所以无效。基于后期的AA应该是可以的。还设想过识别文字形状周围的1像素做alpha混合来实现AA,外轮廓效果应该可以,如下图绿框中的。但就跟透明有类似的排序问题了,且文字之间的边缘原没法处理,如下图红框中的:

  2、遮挡:因为像素的深度值是文字Cube的边界,并不是模型的,一旦有模型穿插到文字体内会不正确,如下图,蓝色部分可见白色cube已经很接近文字cube的前面,但是红框中文字却没被遮挡。PS里写深度、RayMarching里每步测深度、调整渲染顺序应该能解决。

  4、字形过渡:SDF一大特性是能做一个形状到另一个形状的过渡,就是对SDF距离进行过渡,对于本例的文字来说要额外指定另一个字符的UV信息,如果文字Cube大小不一样也需要变化。

  文末,再次感谢燃野的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()。

  也欢迎大家来积极参与U Sparkle开发者计划,简称US,代表你和我,代表UWA和开发者在一起!返回搜狐,查看更多

本文链接:http://sesdagreat.com/jubuzuobiaoxi/83.html

相关推荐:

网友评论:

栏目分类

现金彩票 联系QQ:24498872301 邮箱:24498872301@qq.com

Copyright © 2002-2011 DEDECMS. 现金彩票 版权所有 Power by DedeCms

Top