案例学习——Unity体绘制shader初探
本文的学习素材来源为 Alan Zucconi 的《Volumetric Rendering》一文。
案例学习——Unity体绘制shader初探
-
体绘制概念的引入
- 光线投射算法的基本原理(Ray Casting Algorithm)
- 基于固定步长的Marching Ray算法
- 光线投射算法的基本原理(Ray Casting Algorithm)
-
2 基于距离辅助的光线步进 Distance-Aided Raymarching
-
3 表面颜色设置
-
4 带符号距离函数 (Signed Distance Functions)
-
4.1 概述
-
4.2 集合运算
-
4.3 SDF Box
-
4.4 形状混合 (Shape Blending) (LERP)
-
4.5 平滑合并 (Smooth Union)
-
4.6 SDF 的代数表示法
-
5 环境光遮挡
-
1 体绘制引入
在3D引擎中不管球形几何体方块形状的物体或者其他任何形状的物体都由面元构成因此例如Unity这样的光照系统也只能显示表面的三角形面元。
为了实现对半透明物体材质的表现效果, 经过表面渲染后, 通常采用alpha blend技术将混合后的结果与后续物体的颜色相结合, 从而模拟出类似透明效果的现象
对于GPU来说,整个3D世界 就是一层壳。
显然旨在突破整体界限的挑战,在这个过程中他们采取了多种途径来探索可能性。然而即使最终只能构建出一个基本的外壳框架"但依然有办法深入进去"
例如体积绘制技术(Volume rendering techniques) ,它会模拟光线在其中的传播过程,从而创造出丰富的视觉效果。
我们可以发现Unity中的Unlit的基本的片元着色器模板是这样
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
每个像素都会被片元着色器处理,在相机的观察锥域内分布的三角形将获得相应的颜色值

跳出常规视角的话,在相机捕捉到的某个角度中我们能够观察到这些三角形;而片元着色器的作用则是赋予它们色彩信息。
但其实并不需要仅凭观察到的物体便给予其准确的颜色;我们也可以采用暗示性的方法来赋予物体颜色。
如图所示,观察到的是一个立方体.然而,在一个立方体的表面,我们可以巧妙地为其表面覆盖一层类似于球体的颜色,看上去就像是有一个隐藏的小球在里面一样.

这也即是所谓的立体渲染核心理念,在计算机图形学中通过精细计算模拟光路行为。
为了模仿图示的效果, 我们假设我们的主要几何形状是一个立方体, 并将其内部立体地呈现一个球体. 然而, 实际上并不存在与球相关的mesh数据, 因此我们需要通过Shader代码实现这一视觉效果.
- 球体由一个世界坐标_Centre定义为其中心点,并规定其半径为_Radius。
- 移动立方体现在不改变球体的位置状态,并将其固定在绝对的世界坐标系中。
- 其他几何体也无能为力地影响它的位置状态,在这种情况下由于它是通过虚拟渲染生成的‘无实体’形态的图形元素。
1.1 光线投射算法(Ray Casting)
体绘制的第一种方法,光线投射算法(Ray Casting)
伪代码会像这样
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
}
- 在raycastHit函数中, 基于要渲染的点及其观察方向, 我们便能够确定是否击中了虚拟红色球体。
进而得出球体与射线相交的问题即转化为数学求解过程。
一般性地而言,在Ray Casting算法中存在较低的效能表现。为了实现这一方法的应用,则必须使用相应的数学表达式来计算线段与其自定义几何体之间的交点。然而该种解决方案在模型构建方面存在一定的局限性——它仅支持有限数量的几何形状类型——因此这种方法在实际应用中的适用范围受到很大限制。
1.2 恒定步长的光线步进(Ray Marching with Constant Step)
如同光线投射所带来的局限性,在纯粹的数学解析中分析光线与几何体是否相交并不够灵活。
如果要模拟任何体积,则必须找到一种无需涉及相交的数学方程的方法
Ray Marching(光线步进)就是一种常用的基于迭代方法的技术。
在立方体内逐渐扩散开来。对于每一个步骤,在此期间我们将检测射线是否存在与球体碰撞的可能性。

在实现过程中,我们将每一束光线从当前光线段的位置出发,向观察者视线方向移动一段微小的距离,并每一次移动后检查光线与球心之间的距离是否小于球的半径。
shader实现如下,还是很简单的
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/Volu1"
{
Properties
{
_Centre ("Centre",Vector) = (0,0,0)
_Radius ("Radius", Range(0,1)) = 0.8
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
fixed _Radius;
// 判断是否进入球内
bool sphereHit(float3 p)
{
return distance(p, _Centre) < _Radius;
}
//光线步进
bool raymarchHit(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.1;
for (int i = 0; i < STEPS; i++)
{
if (sphereHit(position))
return true;
position += direction * STEP_SIZE;
}
return false;
}
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
if (raymarchHit(worldPosition, viewDirection))
return fixed4(1,0,0,1); // Red if hit the ball
else
return fixed4(1,1,1,1); // White otherwise
}
ENDCG
}
}
}
虽然数学上判断线段与球体相交困难, 但通过迭代法检测点是否位于球体内却异常简单。观察结果如图所示, 似乎实际上就是一个假想为无光照情况下的立方体内的球体模型。

2 距离辅助的光线步进 Distance Aided Raymarching
固定步长的光线步进在第一节已经实现,但固定步长的话效率不太行。
无论填充体积的这个几何体形状如何,光线每次都会前进相同的量。
何况在shader内添加循环会极大地影响着色器的性能。
如果要使用实时体积渲染,则需要找到更好的更有效的解决方案。
我们致力于寻找一种途径来预测射线能够离开几何形状而不发生碰撞地传播的距离。
为了使该技术起作用,我们需要能够估计与几何体的距离。
我们把之前的判断是否在球内的函数
// 判断是否进入球内
bool sphereHit(float3 p)
{
return distance(p, _Centre) < _Radius;
}
改成估算距离
//估算距离
float sphereDistance(float3 p)
{
return distance(p, _Centre) - _Radius;
}
我们采用了该方法来计算距离,在这一过程中其结果也随之发展成为一个更具技术性的概念——有向距离函数(signed distance functions)
容易理解地说:若计算的结果为正值,则该点位于球外;若计算的结果为负值,则该点位于球内;而当结果等于零时,则该点位于球面上。
它的主要功能是呈现一个较为稳健的距离预估,并告知我们射线在接近球体之前还需要行驶多远的距离。当采用更为复杂的几何形状时,则该技术的价值更加凸显。
此图直观地呈现了其工作原理:当光线接近物体时会尽可能地延伸较远的距离。采用这一策略,则可显著降低光线击中物体所需迭代次数

编写shader如下
Shader "Unlit/Volu1"
{
Properties
{
_Centre ("Centre",Vector) = (0,0,0)
_Radius ("Radius", Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
fixed _Radius;
//估算距离
float sphereDistance(float3 p)
{
return distance(p, _Centre) - _Radius;
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sphereDistance(position);
if (distance < 0.01)
return i / (float)STEPS;
position += distance * direction;
}
return 0;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return (1-raymarch(worldPosition, viewDirection)) * float4(1,1,1,1);
}
ENDCG
}
}
}
详细说明该函数的作用。
当与球不相交时返回0值,在着色过程中将表现为纯白色。
当与球相交时,则通过逐步推进的方式,在设定精度下计算出步长数量,并由此得出一个分数值作为透明度指标。
//估算距离
float sphereDistance(float3 p)
{
return distance(p, _Centre) - _Radius;
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sphereDistance(position);
if (distance < 0.01)
return i / (float)STEPS;
position += distance * direction;
}
return 0;
}
效果如下(花纹为GIF压缩问题,实质为纯色)

每一个步进层级随着着色点的旋转而变大变小

3 表面着色
在本节中主要介绍了估算法线,并将原本的代码行return i / (float)STEPS;改为简单着色模型采用兰伯特与布林结合的方法
兰伯特和布林就不多赘述了,如何估计法线可以聊一聊
原文提出了一种估算法线方向的方法——通过对附近点的距离场进行采样,并计算局部表面的曲率估计值。
每个轴上的差异是通过评估该点在这个轴两侧的距离场来计算的
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sphereDistance(p + float3(eps, 0, 0)) - sphereDistance(p - float3(eps, 0, 0)),
sphereDistance(p + float3(0, eps, 0)) - sphereDistance(p - float3(0, eps, 0)),
sphereDistance(p + float3(0, 0, eps)) - sphereDistance(p - float3(0, 0, eps))
)
);
}
- eps表示用于计算表面坡度的距离。该法线估计技术基于我们正在着色的表面相对光滑这一前提。
对于不连续曲面而言,应用这一方法会导致其坡度无法准确地逼近着色点所对应的法线方向。
完整代码如下
Shader "Unlit/Volu1"
{
Properties
{
_Color("Color",Vector) = (1,1,1)
_Centre ("Centre",Vector) = (0,0,0)
_Radius ("Radius", Range(0,1)) = 0.8
_SpecularPower("SpecularPower",Range(0,20)) = 4
_Gloss("Gloss",Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
fixed _Radius;
fixed _SpecularPower;
fixed _Gloss;
fixed4 _Color;
//估算距离
float sphereDistance(float3 p)
{
return distance(p, _Centre) - _Radius;
}
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sphereDistance(p + float3(eps, 0, 0)) - sphereDistance(p - float3(eps, 0, 0)),
sphereDistance(p + float3(0, eps, 0)) - sphereDistance(p - float3(0, eps, 0)),
sphereDistance(p + float3(0, 0, eps)) - sphereDistance(p - float3(0, 0, eps))
)
);
}
fixed4 simpleLambertBlinn(fixed3 normal,float3 direction) {
fixed3 viewDirection = direction;
fixed3 lightDir = _WorldSpaceLightPos0.xyz; // Light direction
fixed3 lightCol = _LightColor0.rgb; // Light color
fixed NdotL = max(dot(normal, lightDir), 0);
fixed4 c;
// Specular
fixed3 h = (lightDir - viewDirection) / 2.;
fixed s = pow(dot(normal, h), _SpecularPower) * _Gloss;
c.rgb = _Color * lightCol * NdotL + s;
c.a = 1;
return c;
}
fixed4 renderSurface(float3 p, float3 direction)
{
float3 n = normal(p);
return simpleLambertBlinn(n, direction);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sphereDistance(position);
if (distance < 0.01)
//return i / (float)STEPS;
return renderSurface(position, direction);
position += distance * direction;
}
return 1;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return raymarch(worldPosition, viewDirection);
}
ENDCG
}
}
}

4 有向距离函数(Signed Distance Functions)
本节主题是如何在shader中,体绘制出复杂的三维模型。
Signed distance functions (SDF) are utilized to model spheres, cuboids, and toroidal shapes.
相较于传统的基于三角形网格的3D模型而言,有向距离函数不仅具有极高的分辨率,并且特别适合执行几何体操作。
原文展示了一个动画,如何使用简单的形状去创建一个蜗牛:

4.1 介绍
许多现代3D引擎(如Unity)普遍采用三角形来处理几何形状;任何一个复杂的物体不论其复杂程度如何都源自基本的三角形构建。
然而,在计算机图形学中存在这一标准作为实践依据的同时,并非所有的物体都能够仅由三角形构成。例如球体和其他曲面几何体这类形状则难以有效地分解成平面实体。
然而,在表面上密集地覆盖小三角形片以生成一个近似于球体的表面模型,则会带来额外的成本。
有哪些好方法可以替代近似呢?其中之一就是使用有向距离函数(d_{directed}(x,y)),这是表示我们关心的对象所需的数学表达
当将一个球体方程替代成它的几何形状时, 就能消除由近似表示所带来的误差.
我们可以将有向距离函数被视为与矢量图形状相当的三角形,并且能够进行基于SDF定义的几何体缩放操作,并且在缩放过程中保持细节完整性。
无论离边缘有多近,球体永远都是光滑的。
该函数建立在这样一个概念的基础上:每个原始对象都必须定义一个对应的映射关系。也就是说,在每个原始对象上都必须定义一个对应的关系,在此基础上计算出该点与物体表面之间的距离程度。这个过程通过采用3D坐标作为输入,并输出一个数值来实现。
在我们之前的球形判断中,就用到了球的SDF函数(这里改了个名)
float sdf_sphere (float3 p)
{
return distance(p, _Centre) - _Radius;
}
4.2 并集和交集
使用SDF的另一个原因,是因为它们易于合成。
指定两个不同的Sphere Distance Fields(SDFs),我们可以通过某种方式将它们组合成一个新的SDF;计算得到两球体表面最近点的距离值即为此交集区域的特征函数值。
float sdf_sphere(float3 p, float3 c, float r)
{
return distance(p, c) - r;
}
//估算距离
float sphereDistance(float3 p)
{
return min
(
sdf_sphere(p, _Centre1, _Radius1), // Left sphere
sdf_sphere(p, _Centre2, _Radius2) // Right sphere
);
}
效果如下


完整shader如下
Shader "Unlit/Volu1"
{
Properties
{
_Color("Color",Vector) = (1,1,1)
_Centre1("Centre1",Vector) = (1.5,0,0)
_Centre2("Centre2",Vector) = (-1.5,0,0)
_Radius1("Radius1", Range(0,5)) = 0.8
_Radius2("Radius2", Range(0,5)) = 0.8
_SpecularPower("SpecularPower",Range(0,20)) = 4
_Gloss("Gloss",Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre1;
fixed _Radius1;
float3 _Centre2;
fixed _Radius2;
fixed _SpecularPower;
fixed _Gloss;
fixed4 _Color;
float sdf_sphere(float3 p, float3 c, float r)
{
return distance(p, c) - r;
}
//估算距离
float sphereDistance(float3 p)
{
return min
(
sdf_sphere(p, _Centre1, _Radius1), // Left sphere
sdf_sphere(p, _Centre2, _Radius2) // Right sphere
);
}
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sphereDistance(p + float3(eps, 0, 0)) - sphereDistance(p - float3(eps, 0, 0)),
sphereDistance(p + float3(0, eps, 0)) - sphereDistance(p - float3(0, eps, 0)),
sphereDistance(p + float3(0, 0, eps)) - sphereDistance(p - float3(0, 0, eps))
)
);
}
fixed4 simpleLambertBlinn(fixed3 normal,float3 direction) {
fixed3 viewDirection = direction;
fixed3 lightDir = _WorldSpaceLightPos0.xyz; // Light direction
fixed3 lightCol = _LightColor0.rgb; // Light color
fixed NdotL = max(dot(normal, lightDir), 0);
fixed4 c;
// Specular
fixed3 h = (lightDir - viewDirection) / 2.;
fixed s = pow(dot(normal, h), _SpecularPower) * _Gloss;
c.rgb = _Color * lightCol * NdotL + s;
c.a = 1;
return c;
}
fixed4 renderSurface(float3 p, float3 direction)
{
float3 n = normal(p);
return simpleLambertBlinn(n, direction);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sphereDistance(position);
if (distance < 0.01)
//return i / (float)STEPS;
return renderSurface(position, direction);
position += distance * direction;
}
return 1;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return raymarch(worldPosition, viewDirection);
}
ENDCG
}
}
}
4.3 SDF Box
可以看看这个推导
大量几何图形可以通过现有的方法实现构造。为了深入研究,我们需要引入新的SDF原型:占据半个三维空间的基元。从其名称可以看出,这个概念指的是一个仅占有一个半三维空间的基本单元。
考虑点P时,该点到box的间距具体数值是多少?我们专注于右上区域,在第1、2、3空间中该区域与box之间的距离可表示为下图中的三个表达式。

他们可以写成一个表达式

S被定义为盒子(box)的尺寸参数,
C被定义为中心点(center)的位置。
通过计算p减去c得到相对位置,
将该结果减去S即为上述公式的应用方式。
float sdf_box (float3 p, float3 c, float3 s)
{
float x = max
( p.x - c.x - float3(s.x / 2., 0, 0),
c.x - p.x - float3(s.x / 2., 0, 0)
);
float y = max
( p.y - c.y - float3(s.y / 2., 0, 0),
c.y - p.y - float3(s.y / 2., 0, 0)
);
float z = max
( p.z - c.z - float3(s.z / 2., 0, 0),
c.z - p.z - float3(s.z / 2., 0, 0)
);
float d = x;
d = max(d,y);
d = max(d,z);
return d;
}
效果如下

完整代码如下
Shader "Unlit/Volu1"
{
Properties
{
_Color("Color",Vector) = (1,1,1)
_Centre ("Centre",Vector) = (0,0,0)
_Size("Size",Vector) = (1,1,1)
_SpecularPower("SpecularPower",Range(0,20)) = 4
_Gloss("Gloss",Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
fixed3 _Size;
fixed _SpecularPower;
fixed _Gloss;
fixed4 _Color;
float sdf_box(float3 p, float3 c, float3 s)
{
float x = max
(p.x - c.x - float3(s.x / 2., 0, 0),
c.x - p.x - float3(s.x / 2., 0, 0)
);
float y = max
(p.y - c.y - float3(s.y / 2., 0, 0),
c.y - p.y - float3(s.y / 2., 0, 0)
);
float z = max
(p.z - c.z - float3(s.z / 2., 0, 0),
c.z - p.z - float3(s.z / 2., 0, 0)
);
float d = x;
d = max(d, y);
d = max(d, z);
return d;
}
//估算距离
float sdf_Distance(float3 p)
{
return sdf_box(p, _Centre, _Size);
}
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sdf_Distance(p + float3(eps, 0, 0)) - sdf_Distance(p - float3(eps, 0, 0)),
sdf_Distance(p + float3(0, eps, 0)) - sdf_Distance(p - float3(0, eps, 0)),
sdf_Distance(p + float3(0, 0, eps)) - sdf_Distance(p - float3(0, 0, eps))
)
);
}
fixed4 simpleLambertBlinn(fixed3 normal,float3 direction) {
fixed3 viewDirection = direction;
fixed3 lightDir = _WorldSpaceLightPos0.xyz; // Light direction
fixed3 lightCol = _LightColor0.rgb; // Light color
fixed NdotL = max(dot(normal, lightDir), 0);
fixed4 c;
// Specular
fixed3 h = (lightDir - viewDirection) / 2.;
fixed s = pow(dot(normal, h), _SpecularPower) * _Gloss;
c.rgb = _Color * lightCol * NdotL + s;
c.a = 1;
return c;
}
fixed4 renderSurface(float3 p, float3 direction)
{
float3 n = normal(p);
return simpleLambertBlinn(n, direction);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sdf_Distance(position);
if (distance < 0.01)
//return i / (float)STEPS;
return renderSurface(position, direction);
position += distance * direction;
}
return 1;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return raymarch(worldPosition, viewDirection);
}
ENDCG
}
}
}
也有代码更少(但不太精确)的方法
float vmax(float3 v)
{
return max(max(v.x, v.y), v.z);
}
float sdf_boxcheap(float3 p, float3 c, float3 s)
{
return vmax(abs(p-c) - s);
}
4.4 线性插值混合 Shape Blending(lerp)
在alphablending中有一种blending是lerp插值,我们可以用到sdf中来
float sdf_blend(float d1, float d2, float a)
{
return a * d1 + (1 - a) * d2;
}
根据给定的参数d1和d2生成混合(通过控制参数a从0变化到1)。同样的程序不仅适用于颜色融合问题,还可以处理形状融合的情况。例如,以下代码将一个球体融合到一个立方体中:
d = sdf_blend
(
sdf_sphere (p,0 ,r ),
sdf_box (p,0 ,r ),
(_SinTime [ 3 ] + 1. )/ 2。
);
效果如下

完整代码如下
Shader "Unlit/Volu6"
{
Properties
{
_Color("Color",Vector) = (1,1,1)
_Centre("Centre",Vector) = (0,0,0)
_Size("Size",Vector) = (1,1,1)
_SpecularPower("SpecularPower",Range(0,20)) = 4
_Gloss("Gloss",Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
fixed3 _Size;
fixed _SpecularPower;
fixed _Gloss;
fixed4 _Color;
float sdf_box(float3 p, float3 c, float3 s)
{
float x = max
(p.x - c.x - float3(s.x / 2., 0, 0),
c.x - p.x - float3(s.x / 2., 0, 0)
);
float y = max
(p.y - c.y - float3(s.y / 2., 0, 0),
c.y - p.y - float3(s.y / 2., 0, 0)
);
float z = max
(p.z - c.z - float3(s.z / 2., 0, 0),
c.z - p.z - float3(s.z / 2., 0, 0)
);
float d = x;
d = max(d, y);
d = max(d, z);
return d;
}
float sdf_sphere(float3 p,float3 c, float3 s)
{
return distance(p, c) -s.x;
}
float sdf_blend(float d1, float d2, float a)
{
return a * d1 + (1 - a) * d2;
}
//估算距离
float sdf_Distance(float3 p)
{
return sdf_blend
(
sdf_sphere(p, _Centre, _Size),
sdf_box(p, _Centre, _Size),
(_SinTime[3] + 1.) / 2.
);
}
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sdf_Distance(p + float3(eps, 0, 0)) - sdf_Distance(p - float3(eps, 0, 0)),
sdf_Distance(p + float3(0, eps, 0)) - sdf_Distance(p - float3(0, eps, 0)),
sdf_Distance(p + float3(0, 0, eps)) - sdf_Distance(p - float3(0, 0, eps))
)
);
}
fixed4 simpleLambertBlinn(fixed3 normal,float3 direction) {
fixed3 viewDirection = direction;
fixed3 lightDir = _WorldSpaceLightPos0.xyz; // Light direction
fixed3 lightCol = _LightColor0.rgb; // Light color
fixed NdotL = max(dot(normal, lightDir), 0);
fixed4 c;
// Specular
fixed3 h = (lightDir - viewDirection) / 2.;
fixed s = pow(dot(normal, h), _SpecularPower) * _Gloss;
c.rgb = _Color * lightCol * NdotL + s;
c.a = 1;
return c;
}
fixed4 renderSurface(float3 p, float3 direction)
{
float3 n = normal(p);
return simpleLambertBlinn(n, direction);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sdf_Distance(position);
if (distance < 0.01)
//return i / (float)STEPS;
return renderSurface(position, direction);
position += distance * direction;
}
return 1;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return raymarch(worldPosition, viewDirection);
}
ENDCG
}
}
}
4.5 平滑过渡 Smooth Union
前一阶段的结果采用了 lerping 技术, 但事实上还存在多种插值方法. 指数平滑(即 Smooth Minimum 技术)则是其中的一种方法.
float sdf_smin(float a, float b, float k = 32)
{
float res = exp(-k*a) + exp(-k*b);
return -log(max(0.0001,res)) / k;
}
通过这个运算符将两个形状结合在一起时,这两个形状会以柔和的方式融合,并逐渐过渡出一个新的区域来平滑处理所有锋利边缘。

代码如下,主要用了一些sdf的函数
Shader "Unlit/Volu7"
{
Properties
{
_Color("Color",Vector) = (1,1,1)
_Centre("Centre",Vector) = (0,0,0)
_Centre1("Centre1",Vector) = (0,0,0)
_Size("Size",Vector) = (1,1,1)
_SpecularPower("SpecularPower",Range(0,20)) = 4
_Gloss("Gloss",Range(0,1)) = 0.8
}
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION; // Clip space
float3 wPos : TEXCOORD1; // World position
};
float3 _Centre;
float3 _Centre1;
fixed3 _Size;
fixed _SpecularPower;
fixed _Gloss;
fixed4 _Color;
float sdf_sphere(float3 p,float3 c, float3 s)
{
return distance(p, c) - s.x;
}
float opOnion(float sdf, float thickness)
{
return abs(sdf) - thickness;
}
float sdf_smin(float a, float b, float k = 32)
{
float res = exp(-k * a) + exp(-k * b);
return -log(max(0.0001, res)) / k;
}
//估算距离
float sdf_Distance(float3 p)
{
_Centre.y = _SinTime[3] * 8;
return sdf_smin(
opOnion(sdf_sphere(p, _Centre1, _Size), 0.2),
opOnion(sdf_sphere(p, _Centre, _Size), 0.2),
1);
}
float3 normal(float3 p)
{
const float eps = 0.01;
return normalize
(float3
(sdf_Distance(p + float3(eps, 0, 0)) - sdf_Distance(p - float3(eps, 0, 0)),
sdf_Distance(p + float3(0, eps, 0)) - sdf_Distance(p - float3(0, eps, 0)),
sdf_Distance(p + float3(0, 0, eps)) - sdf_Distance(p - float3(0, 0, eps))
)
);
}
fixed4 simpleLambertBlinn(fixed3 normal,float3 direction) {
fixed3 viewDirection = direction;
fixed3 lightDir = _WorldSpaceLightPos0.xyz; // Light direction
fixed3 lightCol = _LightColor0.rgb; // Light color
fixed NdotL = max(dot(normal, lightDir), 0);
fixed4 c;
// Specular
fixed3 h = (lightDir - viewDirection) / 2.;
fixed s = pow(dot(normal, h), _SpecularPower) * _Gloss;
c.rgb = _Color * lightCol * NdotL + s;
c.a = 1;
return c;
}
fixed4 renderSurface(float3 p, float3 direction)
{
float3 n = normal(p);
return simpleLambertBlinn(n, direction);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sdf_Distance(position);
if (distance < 0.01)
//return i / (float)STEPS;
return renderSurface(position, direction);
position += distance * direction;
}
return 0;
}
// Vertex function
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
// Fragment function
fixed4 frag(v2f i) : SV_Target
{
float3 worldPosition = i.wPos;
float3 viewDirection = normalize(i.wPos - _WorldSpaceCameraPos);
return raymarch(worldPosition, viewDirection);
}
ENDCG
}
}
}
4.6 SDF 代数
这些SDF函数及其运算符均属于符号距离函数代数体系中的一部分。
通过该系统可以实现旋转、缩放、弯曲以及扭曲等多种操作。
在《使用距离函数建模》这篇文章中详细介绍了。
SDF无疑是一个极具强大功能的强大工具。
本节仅作为对这一主题的简要概述。
(想起了games102还是201来着……有关几何的部分还没去看……)
5 环境光遮挡
快速的AO可以这么做
思想就是步数消耗越多,AO约明显
fixed4 raymarch (float3 position, float3 direction)
{
for (int i = 0; i < _Steps; i++)
{
float distance = map(position);
if (distance < _MinDistance)
{
fixed4 color = renderSurface(position, direction);
float ao = 1 - float(i) / (_Steps-1);
color.rgb *= ao;
return color;
}
position += distance * direction;
}
return fixed4(1,1,1,1);
}
右边是加了AO的


在SIGGRAPH 2006会议上提出了一个更有效的解决方案(参考文献:)。该方法通过沿着表面法线方向采样距离场来实现。
在检查后未发现任何比当前物体更近的物体,则意味着整个空间内不存在障碍物。我们重复执行指定次数,并且每次都朝着表面方向移动_AOStepSize个单位。
若无法找到更近的目标,则采样距离之和将等于_AOStep乘以_AOStepSize。
这为我们提供了一个可用于插值的值,并进而使得环境光吸收系数介于0与1之间。
float ambientOcclusion(float3 pos, float3 normal)
{
float _AOStep = 30;
float _AOStepSize = 8.5;
float sum = 0;
for (int i = 0; i < _AOStep; i++)
{
float3 p = pos + normal * (i + 1) * _AOStepSize;
sum += sdf_Distance(p);
}
return sum / (_AOStep* _AOStepSize);
}
//光线步进
fixed4 raymarch(float3 position, float3 direction)
{
float STEPS = 64;
float STEP_SIZE = 0.01;
// Loop do raymarcher.
for (int i = 0; i < STEPS; i++)
{
float distance = sdf_Distance(position);
if (distance < 0.01) {
//return i / (float)STEPS;
fixed4 color = renderSurface(position, direction);
float ao = ambientOcclusion(position, normal(position));
color.rgb *= ao;
return color;
}
position += distance * direction;
}
return 0;
}
最右是效果


对于该AO而言,在模型设计中可以应用指数衰减优化
float ambientOcclusion(float3 pos, float3 normal)
{
float sum = 0;
float maxSum = 0;
float _AOStep = 30;
float _AOStepSize = 8.5;
for (int i = 0; i < _AOStep; i++)
{
float3 p = pos + normal * (i + 1) * _AOStepSize;
sum += 1. / pow(2., i) * sdf_Distance(p);
maxSum += 1. / pow(2., i) * (i + 1) * _AOStepSize;
}
return sum / maxSum;
}
如果该过程无法找到更接近的点,则sum将等于maxSum,并未实现AO的目标。
当在AO的目标迭代路径上发现其他物体且distance减少到某个值时,则表明存在更多的遮挡情况。
以下动画显示了效果,最右边那一列


以上是对Alan Zucconi大佬那一系列文章的初步研究,在为后续进行体积大气渲染的技术积累上做了准备。
