主要参考自 https://github.com/SebLague/Ray-Marching
raymarching 的原理是将屏幕像素分成网格,然后以相机作为原点,指向每个网格发出射线,计算射线于场景内物体的交点,遍历所有的网格之后,就完成了整个场景的渲染
关键在于如果计算射线于物体的交点
raymarching 不同于 raytracing 直接计算,则是是沿着射线方向,每次延长固定步长,检测射线是否于场景内的物体相交
不过步长不容易控制,太长容易跳过尺寸小的物体,太短的话效率低,需要很多次才能遍历到远处的物体
通常由于步长的原因,计算和物体相交也并不能准确,可以于定义好的最小距离,如果和物体的距离小于最小的距离,则认为和物体表面相交了
翻译成伪代码大致就是
var startPoint;
var direction;
var step = 64;
var point = startPoint;
for (i = 0; i < step; i++)
{
point += direction;
if (distanceSceneInfo(point) < EPSILON)
{
// hit
break;
}
}
其中计算射线和场景中物体的距离使用的是SDF
https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
这个网址列出了基础几何物体的SDF公式,还有良心的推导视频
下面是球形和立方体的SDF公式
// sphere
float sdSphere(float3 eye, float3 center, float radius)
{
return distance(eye,center)-radius;
}
// cube
float sdCube(float3 eye, float3 center, float3 size)
{
float3 o = abs(eye-center)-size;
float ud = length(max(o,0));
float n = max(max(min(o.x,0),min(o.y,0)), min(o.z,0));
return ud+n;
}
unity 的 postprocess 可以使用自定义的 shader 对每个像素进行处理,就可以模拟屏幕射线,用来实现SDF
先搭建一个简单的场景
加上 postprocess,测试下输出渲染场景深度
先计算以相机为原点发出的射线
然后raymarching遍历场景物体
最后用深度作为颜色输出场景
Shader "Unlit/SDF"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _CameraDepthTexture;
float4x4 _InverseVPMatrix;
float _StepCount;
float _StepSize;
float4 _SphereInfo;
float4 _CubePosInfo;
float4 _CubeScaleInfo;
#define EPSILON 0.05
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
// sphere
float sdSphere(float3 eye, float3 center, float radius)
{
return distance(eye, center)-radius;
}
// cube
float sdCube(float3 eye, float3 center, float3 size)
{
float3 o = abs(eye-center)-size;
float ud = length(max(o,0));
float n = max(max(min(o.x,0),min(o.y,0)), min(o.z,0));
return ud+n;
}
float sdfScene(float3 eye)
{
float ds = sdSphere(eye, _SphereInfo.xyz, _SphereInfo.w * 0.5);
float dc = sdCube(eye, _CubePosInfo.xyz, _CubeScaleInfo.xyz * 0.5);
return min(ds, dc);
}
float raymarching(float3 position, float3 direction)
{
float3 startPoint = position;
float depth = 0;
for (int i = 0; i < _StepCount; i++)
{
startPoint += direction * _StepSize;
if (sdfScene(startPoint) <= EPSILON)
{
depth += 0.01;
}
}
return depth;
}
float4 frag (v2f i) : SV_Target
{
float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depthTextureValue, 1);
float4 worldPos = mul(_InverseVPMatrix, ndc);
worldPos /= worldPos.w;
// return worldPos;
// 相机为射线起点
float3 rayStart = _WorldSpaceCameraPos;
// 物体的世界坐标 - 相机的坐标为射线的方向
float3 direction = normalize(worldPos.xyz - rayStart);
// 遍历场景
float r = raymarching(rayStart, direction);
}
ENDCG
}
}
}
using UnityEngine;
[ExecuteInEditMode]
public class SDF : MonoBehaviour
{
Camera currentCamera;
[SerializeField]
Material effectMaterial;
public GameObject cube;
public GameObject sphere;
public int stepCount; // 遍历次数
public float stepSize; // 遍历步长
private void Awake()
{
currentCamera = GetComponent<Camera>();
currentCamera.depthTextureMode = DepthTextureMode.Depth;
}
private void OnValidate()
{
// 方便调整参数,观察效果
Shader.SetGlobalFloat("_StepCount", stepCount);
Shader.SetGlobalFloat("_StepSize", stepSize);
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (effectMaterial == null)
{
Graphics.Blit(src, dest);
}
else
{
Matrix4x4 viewMat = currentCamera.worldToCameraMatrix;
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(currentCamera.projectionMatrix, false);
Matrix4x4 viewProjMat = (projMat * viewMat);
Shader.SetGlobalMatrix("_InverseVPMatrix", viewProjMat.inverse);
// 场景只有简单的球和立方体
// 传递球和立方体的信息到shader中
Shader.SetGlobalVector("_CubePosInfo", cube.transform.position);
Shader.SetGlobalVector("_CubeScaleInfo", cube.transform.localScale);
Vector3 t = sphere.transform.position;
Vector4 sphereInfo = new Vector4(t.x, t.y, t.z, sphere.transform.localScale.x);
Shader.SetGlobalVector("_SphereInfo", sphereInfo);
Graphics.Blit(src, dest, effectMaterial);
}
}
}