Unity立体渲染系列教程链接:

Unity教程|立体渲染(一)

Unity立体渲染(二)|Raymarching

Unity立体渲染(三)|表面着色


  近期我们将陆续推出专注于立体渲染、光线追踪、表面着色及有向距离场的Unity系列教程。本文是第一篇:立体渲染(Volumetric Rendering)。这些技术能突破现代3D引擎只能渲染物体外壳的最大限制。立体渲染可以实现逼真的材质与灯光进行复杂的交互,例如雾、烟、水和玻璃。查看NMZ的Plasma Globe效果了解立体渲染的基本概念,如下图:

144643ozfkzvl1767k7ju1.png

片段着色器最后返回的对象,是从特定角度看过去特定位置的颜色。这种方式计算的颜色是完全随意的,因此返回的内容可以不必匹配几何体的真实渲染情况。下图展示了一个3D立方体的例子。当片段着色器检测到立方体表面的颜色时,我们获得的颜色如同我们在一个球体上所看到的。这个几何体是个立方体,但是从相机的角度来看,它的外观和感觉其实“酷似”一个球体。

144643ittt2a5jxszstxcs.png

这就是立体渲染(Volumetric Rendering)的基本概念:模拟光线在物体内部的传送。

如果想模拟前面的效果,就要更精确地进行描述。假设主物体是一个立方体,要在其内部立体渲染一个球体,实际上并不存在这个球体,因为我们将完全通过着色器代码来渲染。球体中心点位于_Centre,半径是_Radius,均为世界坐标。移动立方体不会影响球体位置,因为它是完全以世界坐标系来表述的。外部的几何体也不会对该球体造成任何影响。立方体表面的三角形就是通向几何体内部的窗口。虽然可以使用四边形(Quad)减少三角形数量,但要能从立方体的任意角度观看该球体。

立体射线投射
第一种立体渲染的方式完全适用于实现前文所述的效果。片段着色器接收要渲染的点(wolrdPosition)以及视线方向(viewDirection),然后使用raycastHit函数检测是否投射到红色球体。这种技术叫做立体射线投射(Volumetric Raycasting),它将射线从相机投射到几何体内部。
在片段着色器函数中添加剩下的代码:

float3 _Centre; 
float _Radius; 
fixed4 frag (v2f i) : SV_Target 
{ 
float3 worldPosition = ... float3 viewDirection = ... 
if ( raycastHit(worldPosition, viewDirection) ) 
return fixed4(1,0,0,1); 
// Red if hit the ball else return fixed4(1,1,1,1); 
// White otherwise }

下面来解释代码中的变量。
世界坐标
首先,片段的世界坐标就是从相机生成的射线投射到几何体上的点。在片段着色器中获取世界坐标的代码如下:

struct v2f 
{ 
 float4 pos : SV_POSITION; 
 // Clip space float3 wPos : TEXCOORD1; 
 // World position }; 
v2f vert (appdata_full v) 
{ 
 v2f o; 
 o.pos = mul(UNITY_MATRIX_MVP, v.vertex); 
 o.wPos = mul(_Object2World, v.vertex).xyz; return o; 
}


视线方向
其次,视线方向就是射线从相机投射到几何体上被渲染的点的方向。这里需要知道相机坐标,Unity已内置了该变量 _WorldSpaceCameraPos。计算通过两点的射线方向可使用如下代码:

float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);

Raycast Hit函数
当我们知道了渲染点的坐标和方向后,现在需要使用raycastHit函数来决定射线是否投射到了虚拟的红色球体上。这就是球体与线段相交的问题,这种问题已有惯用解决方案,但通常效率不高。如果需要更具分析性的方法,就需要自行解决线段与自定义几何体相交的问题。这种方案极大限制了可以创建的模型,所以很少被应用。

固定步长的立体光线追踪
上面提到纯分析式的立体射线投射,其实不适合解决这里的问题。如果希望模拟任意几何体,就要找到不依赖于相交方程的更为灵活的技术。常见的解决方案叫做立体光线追踪(Volumetric Raymarching),是基于迭代的解决方案。
立体光线追踪会缓慢地将射线投射到立方体内,每一步都会检测当前是否已投射到红色球体。

144643f4ig55t3w3wm75ip.png

每条射线均从片段坐标worldPosition开始,然后迭代沿着viewDirection的方向投射STEP_SIZE单位长度。这可以通过每次迭代为worldPosition加上STEP_SIZE * viewDirection 来实现。
用下面的raymarchHit函数代替之前的raycastHit:

#define STEPS 64 
#define STEP_SIZE 0.01 
bool raymarchHit (float3 position, float3 direction) 
{ 
  for (int i = 0; i < STEPS; i++) 
  { 
    if ( sphereHit(position) ) 
    return true; 
    position += direction * STEP_SIZE; 
  } 
    return false; 
}


下面的函数用于检测点p是否位于球体内:

bool sphereHit (float3 p) 
{ 
  return distance(p,_Centre) < _Radius; 
}

线段与球体相交很难,但迭代检测点是否位于球体内就很简单了。结果见下图,别看它看起来就是个圆形,实际上这就是个无光照的球体:

144745x1l74qwmqln7q7qi.png

Unity教程|立体渲染(一)

Unity立体渲染(二)|Raymarching

Unity立体渲染(三)|表面着色

锐亚教育 锐亚科技 unity unity教程