VolumeRendering(一) 光线步进 RayMarching

这边文章中的代码主要参考:
Volumetric Raymarching Sample
这个Demo中的例子比较复杂,使用SDF生成动态烟雾效果,添加了光照。但在医学领域或其他学科中,我们只是用体绘制来对三维标量场进行可视化,并不需要这么复杂的光照。其中光源可以省略,也不需要复杂的SDF生成烟雾的模型,我们只需要一个立方体就可以了。所以,我写了下面这个demo,便于理解和学习。
Volume Rendering By Ray Marching
以下是简化后的代码,主要去掉了一些动态变化

#define LARGE_NUMBER 1e20
#define MAX_VOLUME_MARCH_STEPS 160//在Volume中光线步进的最大次数
#define MAX_SDF_DETECT_STEPS 15//最大探测次数,用于确定物体边界
#define MARCH_STRIDE 0.4//固定的光线步长(在Volume中)

struct Box{
    vec3 Position;
    vec3 EdgeLength;
};

Box mBox = Box(
    vec3(0,0,0),
    vec3(16.0)
);

struct Camera
{
    vec3 Position;//
    vec3 LookAt;    

    float ImageHeight;//成像高度//根据屏幕长宽比可求宽度
    float FocalDistance;//焦距
};
Camera mCamera = Camera(
    vec3(120, 20, -165),
    vec3(0, 0, 0),
    2.0,
    7.0
);

//https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
float sdBox( vec3 p /*到中心的距离*/, vec3 b/*边长*/ )
{
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float QueryVolumetricDistanceField( in vec3 pos)
{
    float sdfValue= sdBox( pos-  mBox.Position, mBox.EdgeLength);
    return sdfValue;
}

float IntersectRayMarch(in vec3 rayOrigin, in vec3 rayDirection, float maxD)//用于确定物体边界
{
	float precis = MARCH_STRIDE; //这个值太大,会导致边界判断不精确,导致Volume表面有很多曲线形的分界线
    float D = 0.0f;
    for(int i=0; i<MAX_SDF_DETECT_STEPS; i++ )
    {
	    float dis = QueryVolumetricDistanceField( rayOrigin + rayDirection * D);
        if( dis < precis || D > maxD ) 
            break;
        D += dis;
    }
    return  D >= maxD  ? -1.0 : D;//没有碰到物体则返回-1,否则返回深度(据摄像机距离)
}

void SetCamera(in vec2 _uv, in float _aspectRatio, out vec3 _rayOrigin, out vec3 _rayDirection)
{
    float ImageWidth = mCamera.ImageHeight * _aspectRatio;
    
    vec3 ImagePosition = mCamera.Position;
    //vec3 ImagePosition = CameraOrbit(0.3);
    vec3 CameraView = mCamera.LookAt - ImagePosition;
    float ViewLength = length(CameraView);
    vec3 CameraViewDir = CameraView / ViewLength;

    vec3 CameraRight = cross(CameraViewDir, vec3(0, 1, 0));    
    vec3 CameraUp = cross(CameraRight, CameraViewDir);

    vec3 focalPoint = ImagePosition - mCamera.FocalDistance * CameraViewDir;//焦点位置
    
    vec3 ImagePoint = ImagePosition;//用Image的中心点初始化成像点
    //根据uv坐标偏移成像点
    ImagePoint += CameraRight * (_uv.x * 2.0 - 1.0) * ImageWidth *.5;
    ImagePoint += CameraUp * (_uv.y * 2.0 - 1.0) * mCamera.ImageHeight *.5;
    
    _rayOrigin = focalPoint;
    _rayDirection = normalize(ImagePoint - focalPoint);
}
vec3 GetAmbientLight()
{
	return vec3(0.03);
}

vec3 Render( in vec3 rayOrigin, in vec3 rayDirection)
{
    //找到volume的边界(若有外部传入的模型,可以直接得到深度。这里的模型是在shader中用代码生成的,所以使用RayMarch的方式确定边界)
    float volumeDepth = IntersectRayMarch(rayOrigin, rayDirection, LARGE_NUMBER);
    
    vec3 volumetricColor = vec3(0.0f);

    //从Volume的边界开始RayMarch
    if(volumeDepth > 0.0)//若可以触碰到Volume(没有被其他物体遮挡或Volume不在这条路径上)
    {
        float signedDistance = .0;
        for(int i = 0; i < MAX_VOLUME_MARCH_STEPS; i++)
        {
            volumeDepth += max(MARCH_STRIDE, signedDistance);//若还没有到达Volume边界,则先步进到边界处
           // volumeDepth += MARCH_STRIDE;
            vec3 position = rayOrigin + volumeDepth * rayDirection;

            signedDistance = QueryVolumetricDistanceField(position);

			if(signedDistance < 0.0f)//在Volume中
            {
                vec3 conner = mBox.Position-mBox.EdgeLength/2.0;
                float value = texture(iChannel0, (position-conner)/mBox.EdgeLength.x).x;//选取3D纹理的一部分进行采样
                float target =  0.5;//+0.2 * sin(iTime);
               //只显示0.5到0.6之间的值,相当于一个超简单的Transfer Function
                if(value<target||value>target+0.1)
                    value =0.0;
                
                volumetricColor += value * GetAmbientLight();
            }
        }
    }
    return min(volumetricColor, 1.0f);
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    
    float aspectRatio = iResolution.x /  iResolution.y;
    
    vec3 rayOrigin,rayDirection;
    
    SetCamera(
        uv, aspectRatio, //输入
        rayOrigin, rayDirection//输出
        );
        
    vec3 color = Render(rayOrigin, rayDirection);
    
    fragColor=vec4( color, 1.0 );
}

这个直接copy到https://www.shadertoy.com上,在iChannel0中选择Volumes->GrayNoise3D就可以运行了。

这个demo中只有一个pixel shader,所以数据区域是靠SDF和RayMarching生成的,如果有顶点着色器,可以向顶点着色器传入模型数据,那样的话深度的计算就不用这么复杂了。使用SDF的好处是不需要任何顶点数据,就可以在PixelShader中利用RayMarching得到光线与复杂物体的交点,从而进行光线的计算。

接下来分析一下这个Demo中实现的体绘制算法
1.确定光线发射点和发射方向

SetCamera(uv, aspectRatio, //输入
        rayOrigin, rayDirection//输出
        );

在这个简单的场景中,我没有添加其他光源,由数据Volume自己反射环境光,由相机受光体(屏幕)接受光线。根据光线可逆原则,我们可以从屏幕发射光线,经过焦点,射到Volume数据体上。而屏幕可以按焦点对称到焦点和Volume之间。如下图所示
Image是屏幕,ImagePoint是像素,focalPoint是焦点,也是RayOrigin
像素着色器执行一次就是计算一条光线,也就是计算一个像素的颜色。

2.找到光线与volume的交点,也就是光线在Volume上的入射点

float volumeDepth = IntersectRayMarch(rayOrigin, rayDirection, LARGE_NUMBER);

因为没有顶点着色器,没有顶点数据,所以使用立方体的有向距离场(signed distance field)代表数据范围。使用光线步进的方法,从RayOrigin出发,沿着RayDirection不停的向前移动,每次移动的距离就是该点到数据体表面最近的距离,这个距离由SDF得到。Ray Marching Demo For Beginner
这个例子中将算法流程演示的很清楚。
我使用立方体代表数据体。当光线步进的步长小于一个阈值precis的时候,就不再步进了。这个precis很小,此时距离数据体表面已经非常接近了,而这个点就是下一步中RayMarching的起点。

3.从Volume上的入射点开始RayMarching,直到离开数据体

if(volumeDepth > 0.0)//若可以触碰到Volume(没有被其他物体遮挡或Volume不在这条路径上)
    {
        float signedDistance = .0;
        for(int i = 0; i < MAX_VOLUME_MARCH_STEPS; i++)
        {
            volumeDepth += max(MARCH_STRIDE, signedDistance);//若还没有到达Volume边界,则先步进到边界处
           // volumeDepth += MARCH_STRIDE;
            vec3 position = rayOrigin + volumeDepth * rayDirection;

            signedDistance = QueryVolumetricDistanceField(position);

			if(signedDistance < 0.0f)//在Volume中
            {
                vec3 conner = mBox.Position-mBox.EdgeLength/2.0;
                float value = texture(iChannel0, (position-conner)/mBox.EdgeLength.x).x;//选取3D纹理的一部分进行采样
                float target =  0.5;//+0.2 * sin(iTime);
               //只显示0.5到0.6之间的值,相当于一个超简单的Transfer Function
                if(value<target||value>target+0.1)
                    value =0.0;
                
                volumetricColor += value * GetAmbientLight();
            }
        }
    }

在数据体中不断沿着光线方向步进,每到达一个位置,根据该位置在Volume立方体中的相对坐标采样3D纹理(三维数据一般保存在三维纹理中),得到一个value,也就是数据值。根据数据值的大小进行删选,上面的代码中只保留0.5~0.6的值。将这个值乘以环境光得到该位置的亮度,将这条光线上的所有亮度累加,得到该像素的值,也就是着色器的输出值。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值