既然上一个想做的迪斯科灯球不能实现效果,逛了逛shadertoy,突然想起来很久之前就企图学会的raymarching,准备今天再努力努力实现出来,先找一篇大佬的文章参考一下(https://zhuanlan.zhihu.com/p/90245545)
首先是从相机发射射线出去,所以在urp中应该就得用到renderfeature去计算,然后传给shader,之前实现Fog的时候就已经做过这部分的代码了,因为Fog需要重建像素的世界坐标,就用到了射线方向。
一直在想上面那篇文章里的射线可视化是怎么做的,没想明白,我觉得我基础知识还是差了点,所以又找了另一个教程(https://www.youtube.com/watch?v=S8AWd66hoCo&ab_channel=TheArtofCode),跟着敲了一下代码,恍然大悟,以前一直觉得raymarching是个很难懂的东西,但是现在看来还是比较简单的(原理简单一点,实际操作并不简单),因为之前写过C#窗体光线追踪的代码,所以理解raymarching不难,难的是我没明白unity里用shader怎么写,经过这个视频的洗礼,加上以前的基础,总算是摸到raymarching的门槛了
放一下跟着大佬做出来的效果:
理论:从一个起点朝一个方向发射射线,这条射线会以一定的步长前进,然后检测射线终点位置是否位于物体表面,若不在则根据距离物体的实际距离更新步长,直到击中物体,再计算击中点的颜色就行了。需要注意这些计算要在同一个空间下,否则可能会产生问题。
实践完成一下自己的raymarching效果:
先来个基础的shader,并加上了一些步进需要使用的属性
Shader "Custom/RayMarchingTest"
{
Properties
{
_BaseMap ("BaseMap", 2D) = "white" {}
MAX_STEPS("MAX_STEPS",float)=100//步进最大次数
SURF_DIST("SURF_DIST",float)=0.001//距离容差值
MAX_DIST("MAX_DIST",float)=100//步进的最远距离
}
SubShader
{
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float MAX_STEPS;
float SURF_DIST;
float MAX_DIST;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes{
float4 positionOS : POSITION;
float4 texcoord : TEXCOORD;
};
struct Varyings{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD2;
};
ENDHLSL
Pass
{
Tags{"LightMode" = "UniversalForward"}
Cull Off
HLSLPROGRAM
#pragma target 3.0
#pragma vertex Vertex
#pragma fragment Frag
Varyings Vertex(Attributes input)
{
Varyings output;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.positionCS = vertexInput.positionCS;
output.uv = input.texcoord.xy;
return output;
}
float4 Frag(Varyings input):SV_Target
{
float4 tex = SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,input.uv);
return tex;
}
ENDHLSL
}
}
Fallback "Universal Render Pipeline/Lit"
}
然后操作基本就是在片元着色器中进行,
首先确定计算的空间为世界空间
然后要设定射线的起点和方向,起点比较简单,一般就是选择摄像机的位置
float3 rayOrigion = _WorldSpaceCameraPos;//射线起点
因为是在一个物体上挂载的这个shader,只能通过这个物体来看到效果,所以直接使用原本这个物体要渲染的片元所在位置减相机位置来计算方向就好了,如果是用renderfeature的话,就可以跟做Fog的后处理效果一样,插值得到相机到近平面像素的射线,那样就可以用相机观察到这个虚拟空间了。
这里两种方法我都做个效果展示:
shader挂载在物体上:
shader挂载在后处理上:
我选择挂载在物体上,所以方向的计算如下:
float3 rayDirection = normalize(input.positionWS-rayOrigion);
接下来就是要定义RayMarch函数,将射线的起点和方向作为参数,RayMarch负责发射射线并处理是否碰撞物体,返回一个float值,这个值会有两种可能,一种是在允许误差内击中物体的点到起点的距离,或者没有击中物体的超过最大值的一个值。用这个值就可以判断是否碰撞到物体,如果碰撞到就返回一个颜色,先来个简单的白色,如果没碰撞到,可以用discard来展示一个透明的效果。
所以片元着色器中的代码基本就成型了:
float4 Frag(Varyings input):SV_Target
{
float3 rayOrigion = _WorldSpaceCameraPos;
float3 rayDirection = normalize(input.positionWS-rayOrigion);
float dis = RayMarch(rayOrigion,rayDirection);
float4 col=0;
if(dis<MAX_DIST)
col = 1;
else
discard;
return col;
}
接下来就是实现这个RayMarch函数:
//光线步进
float RayMarch(float3 rayOrigion,float3 rayDirection )
{
float disFromOrigion = 0;//终点距离射线起点的距离
float disFromSphere;//距离物体的距离
for(int i=0;i<MAX_STEPS;i++)
{
float3 position = rayOrigion + rayDirection * disFromOrigion;//射线目前的终点坐标
disFromSphere = GetDist(position);//该点离物体的距离
disFromOrigion+=disFromSphere;//用计算出来的离物体的距离更新步长
if(disFromSphere<SURF_DIST || disFromOrigion>MAX_DIST) break;//如果该点离物体的距离非常小或者该点到起点的距离超过了最大值,就不继续前进了
}
return disFromOrigion;//返回击中物体的点到起点的距离或者返回一个超过最大距离的值-表明没击中物体
}
代码里的注释已经写得很详细了,虽然可能表述有点乱……
然后可以看到有一个函数GetDist还没有实现,这个函数是用来计算离物体的距离的,也可以用来判断该射线目前是否击中物体,所以它的作用就是和光线追踪里面的hit函数一样,因此学习过光线追踪的我知道首先要做的就是定义一个物体,最简单的当然是球体了,当年光线追踪也是从球体开始的,所以先来看一下GetDist怎么写吧
float GetDist(float3 pos)
{
float d=length(pos)-0.5;//这里计算pos点离一个圆心在0,0,0,半径为0.5的球体的距离
return d;
}
这部分代码很好理解,首先把d看做0,把length的实际运算写出来,就可以得到 sqrt(pos.x^2 + pos.y^2 +pos.z^2)-0.5=0,再简化一点就是x^2+y^2+z^2=0.5^2 而这个东西就是圆心在(0,0,0),半径为0.5的球体的定义,由此可以看得出来,其他常见的几何图形也是可以计算的,不常见的几何图形通过复杂的数学运算也是可以实现的。
以上的代码完成后,就可以通过挂载了这个shader的物体看到这个我们定义的球,不过因为我返回的是纯色,所以立体感不是很强,在渲染中,我觉得立体感就是法线和光线的交互计算带给我们的,所以计算一个法线会让这个球看起来好一些
//计算法线
float3 GetNormal(float3 position)
{
return normalize(position);
}
由于我们物体定义的很简单,所以法线的获得也是很简单的,再更新一下片元着色器的代码就可以得到一个彩色的球了
float4 Frag(Varyings input):SV_Target
{
float3 rayOrigion = _WorldSpaceCameraPos;
float3 rayDirection = normalize(input.positionWS-rayOrigion);
float dis = RayMarch(rayOrigion,rayDirection);
float4 col=0;
if(dis<MAX_DIST)
{
float3 position = rayOrigion + rayDirection * dis;
col.rgb = GetNormal(position);
}
else
discard;
return col;
}
最终效果(截图软件有点问题,实际看到的球法线是正常的……)
当然了这只是踏入RayMarch的第一步,还要学习更多来写过更炫酷的效果
题外话:最近一直在看绝命毒师,今天看到大结局了,我发现,有些“坏人”在“更坏的人”的衬托下仿佛像个好人,人性还是不要试探的好,好好生活,死亡总是要来临的,不论是今天还是明天或者还有很多年,要有随时死掉的心理准备,就能更好的爱身边的人啦!!!!
哦对了,忘了给完整代码了,当当当~
Shader "Custom/RayMarchingTest"
{
Properties
{
_BaseMap ("BaseMap", 2D) = "white" {}
MAX_STEPS("MAX_STEPS",float)=100//步进最大次数
SURF_DIST("SURF_DIST",float)=0.001//距离容差值
MAX_DIST("MAX_DIST",float)=100//步进的最远距离
}
SubShader
{
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float MAX_STEPS;
float SURF_DIST;
float MAX_DIST;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes{
float4 positionOS : POSITION;
float4 texcoord : TEXCOORD;
};
struct Varyings{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float2 uv : TEXCOORD1;
};
ENDHLSL
Pass
{
Tags{"LightMode" = "UniversalForward"}
Cull Off
HLSLPROGRAM
#pragma target 3.0
#pragma vertex Vertex
#pragma fragment Frag
Varyings Vertex(Attributes input)
{
Varyings output;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.positionCS = vertexInput.positionCS;
output.uv = input.texcoord.xy;
output.positionWS = vertexInput.positionWS;
return output;
}
float GetDist(float3 pos)
{
float d=length(pos)-0.5;//这里计算pos点离一个圆心在0,0,0,半径为0.5的球体的距离
return d;
}
//光线步进
float RayMarch(float3 rayOrigion,float3 rayDirection )
{
float disFromOrigion = 0;//终点距离射线起点的距离
float disFromSphere;//距离物体的距离
for(int i=0;i<MAX_STEPS;i++)
{
float3 position = rayOrigion+rayDirection*disFromOrigion;//射线目前的终点坐标
disFromSphere = GetDist(position);//该点离物体的距离
disFromOrigion+=disFromSphere;//用计算出来的离物体的距离更新终点
if(disFromSphere<SURF_DIST || disFromOrigion>MAX_DIST) break;//如果该点离物体的距离非常小或者该点到起点的距离超过了最大值,就不继续前进了
}
return disFromOrigion;//返回击中物体的点到起点的距离或者返回一个超过最大距离的值-表明没击中物体
}
//计算法线
float3 GetNormal(float3 position)
{
return normalize(position);
}
float4 Frag(Varyings input):SV_Target
{
float3 rayOrigion = _WorldSpaceCameraPos;
float3 rayDirection = normalize(input.positionWS-rayOrigion);
float dis = RayMarch(rayOrigion,rayDirection);
float4 col=0;
if(dis<MAX_DIST)
{
float3 position = rayOrigion + rayDirection * dis;
col.rgb = GetNormal(position);
}
else
discard;
return col;
}
ENDHLSL
}
}
Fallback "Universal Render Pipeline/Lit"
}
M·M明天还要继续努力,今天稍微浪费了点时间!!!