Volume Rendering 101
原文链接 https://graphicsrunner.blogspot.com/2009/01/volume-rendering-101.html
Pictures above from: http://www.cs.utah.edu/~jmk/simian/
有相当多的文件和论文的体积渲染。但是(就我所见)关于这个主题并没有很多好的教程。因此,本教程将尝试教授体渲染的基础知识,更具体地说体射线投射(Volume ray-casting)(或体射线行进,Volume ray-marching)。
体绘制是一种直接显示3D标量场而无需首先将中间表示形式拟合到数据(例如三角形)的方法。 我们如何渲染没有几何体的体积? 有两种传统的渲染体积的方法:基于切片的渲染和体积光线投射。 本教程将重点介绍体积射线投射。 与光线投射提供的基于片段的渲染相比,有很多优点; 例如跳过空白空间,独立于投影,易于实现以及单次通过。
体积射线投射(也称为射线行进)就是它的发音。 [编辑:体积射线投射与ala Doom或Nick的教程中的射线投射不同]射线投射在体积中,并以相等的间隔采样。 当光线行进通过体积时,通过使用传递函数将标量值映射到光学属性,该传递函数将导致RGBA颜色值,该值包括当前采样点的相应发射和吸收系数。 然后使用从前到后或从后到前的alpha混合来合成此颜色。
本教程将特别着重于如何将射线与体积相交并将其行进穿过体积。 在另一个教程中,我将重点介绍传递函数和着色。 首先,我们需要知道如何读取数据。 数据只是存储为切片[x,y,z]的标量值(通常是整数或浮点数),其中x =宽度,y =高度,z =深度。 每个切片的宽度为x个单位,高度为y个单位,切片的总数等于z。 数据的通用格式将以8位或16位RAW格式存储。 有了数据后,我们需要将其加载到卷纹理中。 这是我们整个过程的方式:
//create the scalar volume texturem
Volume = new Texture3D(Game.GraphicsDevice, mWidth, mHeight, mDepth, 0,
TextureUsage.Linear, SurfaceFormat.Single);
private void loadRAWFile8(FileStream file)
{
BinaryReader reader = new BinaryReader(file);
byte[] buffer = new byte[mWidth * mHeight * mDepth];
int size = sizeof(byte);
reader.Read(buffer, 0, size * buffer.Length);
reader.Close();
//scale the scalar values to [0, 1]
mScalars = new float[buffer.Length];
for (int i = 0; i < buffer.Length; i++)
{
mScalars[i] = (float)buffer[i] / byte.MaxValue;
}
mVolume.SetData(mScalars);
mEffect.Parameters["Volume"].SetValue(mVolume);
}
为了渲染该纹理,我们拟合了一个边界框或立方体,即从体积的[0,0,0]到[1,1,1]。 然后我们渲染立方体并采样体积纹理以渲染体积。 但是,我们还需要找到从眼睛/相机开始并与立方体相交的光线的方法。
通过在着色器中执行射线-立方体相交,我们可以计算出从眼睛到当前像素位置与立方体的光线的相交。 但是,执行此操作的更好、更快的方法是将立方体的正面和背面三角形的位置渲染为纹理。 这很容易为我们提供光线的开始和结束位置,并且在着色器中,我们仅对纹理进行采样即可找到采样光线。 这是纹理的外观(正面,背面,光线方向):
这是呈现前后位置的代码:
//draw front faces
//draw the pixel positions to the textureGame.GraphicsDevice.SetRenderTarget(0, mFront);
Game.GraphicsDevice.Clear(Color.Black);
base.DrawCustomEffect();
Game.GraphicsDevice.SetRenderTarget(0, null);
//draw back faces
//draw the pixel positions to the textureGame.GraphicsDevice.SetRenderTarget(0, mBack);
Game.GraphicsDevice.Clear(Color.Black);
Game.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
base.DrawCustomEffect();
Game.GraphicsDevice.SetRenderTarget(0, null);
Game.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
现在,为了执行体积的实际光线投射,我们渲染了立方体的正面。 在着色器中,我们对前后位置纹理进行采样,以找到将对体积进行采样的射线的方向(后-前)和起始位置(前)。 然后,通过沿等距距离沿射线前进当前采样位置,对体积进行迭代采样。 并且我们使用从前到后的合成来累积像素颜色。
float4 RayCastSimplePS(VertexShaderOutput input) : COLOR0{
//calculate projective texture coordinates
//used to project the front and back position textures onto the cube
float2 texC = input.pos.xy /= input.pos.w;
texC.x = 0.5f*texC.x + 0.5f;
texC.y = -0.5f*texC.y + 0.5f;
float3 front = tex2D(FrontS, texC).xyz;
float3 back = tex2D(BackS, texC).xyz;
float3 dir = normalize(back - front);
float4 pos = float4(front, 0);
float4 dst = float4(0, 0, 0, 0);
float4 src = 0;
float value = 0;
float3 Step = dir * StepSize;
for(int i = 0; i < Iterations; i++)
{
pos.w = 0;
value = tex3Dlod(VolumeS, pos).r;
src = (float4)value;
src.a *= .5f; //reduce the alpha to have a more transparent result
//Front to back blending
// dst.rgb = dst.rgb + (1 - dst.a) * src.a * src.rgb
// dst.a = dst.a + (1 - dst.a) * src.a
src.rgb *= src.a;
dst = (1.0f - dst.a)*src + dst;
//break from the loop when alpha gets high enough
if(dst.a >= .95f)
break;
//advance the current position
pos.xyz += Step;
//break if the position is greater than <1, 1, 1>
if(pos.x > 1.0f pos.y > 1.0f pos.z > 1.0f)
break;
}
return dst;
}
下面是采样的结果:一只脚,一个里面有龙虾的茶壶,引擎,盆景树,动脉瘤的ct扫描,头骨和泰迪熊。
项目源码:https://onedrive.live.com/authkey=%21AJM7E5k%5FxeKVfss&id=B80A3031B5BFA52B%21123&cid=B80A3031B5BFA52B