光线追踪和球体追踪

光线追踪和球体追踪

#光线追踪 #球体追踪

参考:

《The Unity Shader Bible》

RayMarching入门 SphereTracing

光线追踪的概念

 

image.png

 

光线从摄像机出发,穿过像素点,直到击中表面以达到目标。这个概念被称为“光线投射(Ray Marching)” ,即沿着光线找到最近物体的过程,因此得名“球面追踪(Sphere Tracing)”。 RayMarching就是光线行进,从摄像机向屏幕上的每一个像素发射一条光线,光线按照一定步长前进,检测当前光线距离物体表面的距离,并且根据这个距离调整光线的步长,直到抵达物体表面。

SDF

我们需要根据即将生成的曲面形状来确定射线的终点。在此场景中,这类名为SDF(有符号距离函数)的函数会以点为输入,返回该点与图形表面之间的最短距离。若返回值为正,则射线继续前进;若返回值为零,则射线将与曲面发生碰撞。

 
 
 
xxxxxxxxxx
 
 
 
 
float sphereSDF(float3 p, float radius) { 
    float sphere = length(p) - radius;
    return sphere;
}
 

球体追踪

着色器应用

为了正确使用该技术,我们需要在着色器中定义至少两个函数。为此,需要考虑以下因素: • 一个用于确定曲面类型的sdf函数. • 另一个用于计算球体追踪的函数.

image.png

 

流程图

 
 
 
 
 
 
起点 ray_origin
计算 ray_position
调用 planeSDF ray_position
得到最近距离 d
distance_origin += d
d < SURFACE_DISTANCE?
命中物体 结束
distance_origin > MAX_MARCHING_STEPS?
未命中 空

核心Shader代码

球体追踪就是用场景的 SDF(signed distance function)告诉你从当前位置到最近表面的最小距离,然后用这个距离作为步长向前跳——仿佛每一步放一个贴紧场景的球体,球体刚好不穿透物体,直到球体接触表面或超过上限。

 
 
 
x
 
 
 
 
//平面SDF方程 
float planeSDF(float3 ray_position) {  
    // subtract the edge to the “Y” ray position to increase  
    //or decrease the plane position    
    float plane = ray_position.y - _Edge;  
    return plane;  
}  
  
#define MAX_MARCHING_STEPS 50  
#define MAX_DISTANCE 10  
#define SURFACE_DISTANCE 0.001  

//球体追踪函数
float sphereCasting(float3 ray_origin, float3 ray_direction)  
{  
    float distance_origin = 0;  
    for (int i = 0; i < MAX_MARCHING_STEPS; i++)  
    {        
        float3 ray_position = ray_origin + ray_direction * distance_origin;  
        float distance_scene = planeSDF(ray_position);  
        distance_origin += distance_scene;        
        if (distance_scene < SURFACE_DISTANCE || distance_origin > MAX_MARCHING_STEPS)  
            break;  
    }    
    return distance_origin;  
}
 

“射线起始点”向量对应“射线的起点”,即相机在局部空间中的位置;而“射线方向”则等于“网格顶点的位置”,也就是我们正在处理的球体所在位置。由于我们需要根据边生成图元划分,因此必须确保sdf平面的位置与三维物体的位置保持一致。 💁在该示例中,相机到平面的距离即float plane = ray_position.y - _Edge; 为射线步进的长度 image.png

这段代码是一个 Sphere Tracing(球面追踪 / Ray Marching) 的核心片段,用来做 基于 SDF(有符号距离函数) 的光线行进算法。我们逐行拆解:

🔎 核心概念

  1. ray_origin
    光线的起点(比如相机位置)。

  2. ray_direction
    光线的方向(归一化向量)。

  3. distance_origin
    当前光线走了多远。

  4. planeSDF(ray_position)

    • SDF = Signed Distance Function(有符号距离函数)。
    • 返回当前位置到场景表面的最近距离。
    • 如果 >0 → 在表面外;
      如果 <0 → 穿进去了(通常表示点在物体内部)。
  5. Sphere Tracing 原理
    因为 SDF 告诉我们“离物体最近的距离”,所以每次可以大胆走这么远,而不会穿过物体。
    这就像拿着一个“安全半径球”在前进,保证不会撞墙。

🌀 循环逻辑

  • 计算当前位置

     
     
     
    xxxxxxxxxx
     
     
     
     
    ray_position = ray_origin + ray_direction * distance_origin;
     
  • 计算到物体的最近距离

     
     
     
    xxxxxxxxxx
     
     
     
     
    distance_scene = planeSDF(ray_position);
     
  • 向前走这么远

     
     
     
    xxxxxxxxxx
     
     
     
     
    distance_origin += distance_scene;
     
  • 收敛条件

    • 如果 distance_scene < SURFACE_DISTANCE,说明非常接近表面,可以认为击中了物体。
    • 如果 distance_origin > MAX_MARCHING_STEPS,说明走太远还没碰到 → 判定为空。

🎮 类比理解

想象你在 漆黑的房间里摸墙

  • 你知道离墙大概多远(SDF 提供的距离)。
  • 于是你可以一下子走这段距离(安全前进)。
  • 如果你发现已经非常接近墙面,就停下(命中)。
  • 如果走了很久还没遇到墙 → 前面啥也没有。

如前所述,需要剔除球体平面上的像素点。要实现这种评估,我们需要获取球体y轴上顶点在物体空间中的坐标位置。通过使用“discard”语句,我们可以剔除位于边缘的像素点,具体操作如下所示。

 
 
 
xxxxxxxxxx
 
 
 
 
if(vertexPosition.y>_Edge)
    discard;
 

示例:橘子瓤

基础绘制

 
 
 
xxxxxxxxxx
 
 
 
 
Shader "Unlit/FruitSDF"  
{  
Properties  
{  
_MainTex ("Texture", 2D) = "white" {}  
_Edge("Edge", Range(-0.5, 0.5))=0.0  
}  
SubShader  
{  
  
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;  
    float3 hitPos : TEXCOORD1;  
};  
  
sampler2D _MainTex;  
float4 _MainTex_ST;  
float _Edge;  
  
// declare the function for the plane  
float planeSDF(float3 ray_position)  
{  
    // subtract the edge to the “Y” ray position to increase    
    // // or decrease the plane position                
    float plane = ray_position.y - _Edge;    
    return plane;  
}  
  
#define MAX_MARCHING_STEPS 50  
#define MAX_DISTANCE 10  
#define SURFACE_DISTANCE 0.001  
  
float sphereCasting(float3 ray_origin, float3 ray_direction)  
{  
    float distance_origin = 0;  
    for (int i = 0; i < MAX_MARCHING_STEPS; i++)  
    {        
        float3 ray_position = ray_origin + ray_direction * distance_origin;  
        float distance_scene = planeSDF(ray_position);  
        distance_origin += distance_scene;        
        if (distance_scene < SURFACE_DISTANCE || distance_origin > MAX_MARCHING_STEPS)  
            break;  
    }   
    return distance_origin;  
}  
  
v2f vert(appdata v)  
{  
    v2f o;  
    o.vertex = UnityObjectToClipPos(v.vertex);  
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);  
    o.hitPos = v.vertex;  
    return o;  
}  
  
fixed4 frag(v2f i) : SV_Target  
{  
    // sample the texture    
fixed4 col = tex2D(_MainTex, i.uv);  
  
    //最后,我们可以丢弃位于边缘属性上的像素。    
if (i.hitPos.y > _Edge)  
        discard;  
  
    return col;  
}  
ENDCG  
}  
}}
 

image.png

开启双面显示

 
 
 
xxxxxxxxxx
 
 
 
 
SubShader  
{  
    ...
    //关闭裁剪,显示双面
    cull Off  
    
    ...
  
    Pass    {
        ...
    }
 

image.png

获取切面

我们可以将球体的像素投影到其背面,对于平面,可以使用sv_isfrontface语义将其投影到正面。

 
 
 
xxxxxxxxxx
 
 
 
 
fixed4 frag (v2f i, bool face: SV_IsFrontFace) : SV_Target  
{  
    // sample the texture  
    fixed4 col = tex2D(_MainTex, i.uv);  
  
    //将相机位置转换到物体空间  
    float3 ray_origin = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));  
  
    //计算射线方向  
    float3 ray_direction = normalize(i.hitPos - ray_origin);  
  
    //调用球体投射函数  
    float t = sphereCasting(ray_origin, ray_direction);  
  

    float3 p= ray_origin+ ray_direction*t;  
  
    //最后,我们可以丢弃位于边缘属性上的像素。  
    if(i.hitPos.y>_Edge)  
        discard;  
  
    return face?col:float4(p,1);  
}
 

image.png

UV和贴图

在这种情况下,如果要在sdf平面上投影纹理,就需要考虑sdf平面背面的位置和方向——记住这个平面是朝向正y轴的。因此,纹理的方向需要与之相反。 image.png

为此,我们可以使用点“p.xz.”计算最大球面投射区域内的uv坐标。 UVhandler.png

交点局部坐标映射为UV坐标

  • 交点 = 相机位置 + 射线方向 × 距离
  • 交点坐标的投影(如 x-z)可以作为 UV 变换的初始值
 
 
 
xxxxxxxxxx
 
 
 
 
fixed4 frag (v2f i, bool face: SV_IsFrontFace) : SV_Target  
{  
    // sample the texture  
    fixed4 col = tex2D(_MainTex, i.uv);  
  
    //将相机位置转换到物体空间  
    float3 ray_origin = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));  
  
    //计算射线方向  
    float3 ray_direction = normalize(i.hitPos - ray_origin);  
  
    //调用球体投射函数  
    float t = sphereCasting(ray_origin, ray_direction);  
  
    float4 planeCol=0;  
    if(t<MAX_DISTANCE)  
    {        //计算交点位置  
        float3 plane_pos = ray_origin+ ray_direction*t;  
        float2 uv_plane = float2(plane_pos.x, plane_pos.z);  
        planeCol = tex2D(_PlaneTex, uv_plane);  
    }  
  
    //最后,我们可以丢弃位于边缘属性上的像素。  
    if(i.hitPos.y>_Edge)  
        discard;  
  
    return face?col:planeCol;  
}
 

image.png

进一步,将UV居中

 
 
 
xxxxxxxxxx
 
 
 
 
planeCol = tex2D(_PlaneTex, uv_plane-0.5);
 

_Edge=0.43

image.png

_Edge=0.02 image.png

让UV适配球体

 
 
 
xxxxxxxxxx
 
 
 
 
float uvScale= sqrt(1-pow((_Edge*2),2));
 

修改边属性值后,我们将看到平面的纹理投影会根据球体的体积而逐渐减小。

 
 
 
xxxxxxxxxx
 
 
 
 
if(t<MAX_DISTANCE)  
{  
    //计算交点位置  
    float3 plane_pos = ray_origin+ ray_direction*t;  
    //黑洞  
    //float uvScale= sqrt(1-pow((i.hitPos.y-0.05)*2,2));  
    float uvScale= sqrt(1-pow((_Edge*2),2));  
     float2 uv_plane= (plane_pos.xz/uvScale)+0.5;   
planeCol = tex2D(_PlaneTex, uv_plane);  
}
 

桔子.png

橘子皮.png fruit.gif

 

边缘绘制

 
 
 
xxxxxxxxxx
 
 
 
 
  
float4 planeCol=0;  
float4 circleCol=0;  
  
if(t<MAX_DISTANCE)  
{  
    //计算交点位置  
    float3 plane_pos = ray_origin+ ray_direction*t;  
    //黑洞  
    //float uvScale= sqrt(1-pow((i.hitPos.y-0.05)*2,2));  
    //利用平面参数调整uv缩放    float uvScale= sqrt(1-pow((_Edge*2),2));  
  
    float c= length(plane_pos.xz);  
  
    circleCol= smoothstep(c-0.001,c+0.01,_CircleRad-abs(pow(_Edge*(1*0.5),2)));  
    float2 uv_plane= (plane_pos.xz/uvScale)+0.5;   
//planeCol = tex2D(_PlaneTex, uv_plane);  
    planeCol = circleCol;  
}
 

image.png

最终效果

image.png

 
 
 
 
 
 
Shader "Unlit/FruitSDF"  
{  
    Properties  
    {  
        _MainTex ("Texture", 2D) = "white" {}  
        // plane texture   
_PlaneTex ("Plane Texture", 2D) = "white" {}  
        // edge color projection   
_CircleCol ("Circle Color", Color) = (1, 1, 1, 1)  
        // edge radius projection   
_CircleRad ("Circle Radius", Range(0.0, 0.5)) = 0.45  
        _Edge("Edge", Range(-0.5, 0.5))=0.0  
    }  
    SubShader  
    {  
        cull Off  
  
        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;  
                float3 hitPos : TEXCOORD1;  
            };  
            sampler2D _MainTex;  
            float4 _MainTex_ST;  
            sampler2D _PlaneTex;  
            float4 _CircleCol;  
            float _CircleRad;  
            float _Edge;  
  
            // declare the function for the plane  
            float planeSDF(float3 ray_position) {  
                // subtract the edge to the “Y” ray position to increase  
                // // or decrease the plane position                float plane = ray_position.y - _Edge;  
                return plane;  
            }  
            #define MAX_MARCHING_STEPS 50  
            #define MAX_DISTANCE 10  
            #define SURFACE_DISTANCE 0.001  
  
            float sphereCasting(float3 ray_origin, float3 ray_direction)  
            {                float distance_origin = 0;  
                for (int i = 0; i < MAX_MARCHING_STEPS; i++)  
                {                    float3 ray_position = ray_origin + ray_direction * distance_origin;  
                    float distance_scene = planeSDF(ray_position);  
                    distance_origin += distance_scene;                    if (distance_scene < SURFACE_DISTANCE || distance_origin > MAX_MARCHING_STEPS)  
                        break;  
                }                return distance_origin;  
            }  
            v2f vert (appdata v)  
            {                v2f o;  
                o.vertex = UnityObjectToClipPos(v.vertex);  
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);  
                o.hitPos= v.vertex;  
                return o;  
            }  
            fixed4 frag (v2f i, bool face: SV_IsFrontFace) : SV_Target  
            {  
                // sample the texture  
                fixed4 col = tex2D(_MainTex, i.uv);  
  
                //将相机位置转换到物体空间  
                float3 ray_origin = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));  
  
                //计算射线方向(相机指向片元 i.hitPos顶点着色器顶点信息传入片元)  
                float3 ray_direction = normalize(i.hitPos - ray_origin);  
  
                //调用球体投射函数  
                float t = sphereCasting(ray_origin, ray_direction);  
  
                float4 planeCol=0;  
                float4 circleCol=0;  
                if(t<MAX_DISTANCE)  
                {                    //计算交点位置  
                    float3 plane_pos = ray_origin+ ray_direction*t;  
                    //黑洞  
                    //float uvScale= sqrt(1-pow((i.hitPos.y-0.05)*2,2));  
                    //利用平面参数调整uv缩放                    float uvScale= sqrt(1-pow((_Edge*2),2));  
  
                    //计算圆形边缘颜色  
                    float c= length(plane_pos.xz);  
                    circleCol= smoothstep(c-0.001,c+0.01,_CircleRad-abs(pow(_Edge*(1*0.5),2)));  
                    float2 uv_plane= (plane_pos.xz/uvScale)+0.5;  
                    planeCol = tex2D(_PlaneTex, uv_plane);  
                    planeCol*= circleCol;  
                    //叠加  
                    planeCol+= (1-circleCol)*_CircleCol;  
                }  
  
                //最后,我们可以丢弃位于边缘属性上的像素。  
                if(i.hitPos.y>_Edge)  
                    discard;  
  
                //正面返回模型纹理;背面返回平面纹理  
                return face?col:planeCol;  
            }            ENDCG  
        }  
 
 
    }}
 

 

posted @ 2025-09-10 15:26  世纪末の魔术师  阅读(5)  评论(0)    收藏  举报