CodeReader - easy volume renderer及简单优化

Easy Volume Renderer | VFX 着色器 | Unity Asset Store

mlavik1/UnityVolumeRendering: Volume rendering, implemented in Unity3D. Want to support my project? Donate some money to Red Cross and send me a screenshot/message/issue, and I'll be greatly motivated! 😃 Any amount is welcome! (github.com)

该插件提供了简单的体渲染的实现。本文阅读代码主要是为了了解在unity中如何实现volume render。该插件使用了openDicom读取dicom数据。插件提供了直接渲染之外,还提供了切面,截面,

DirectVolumeRendering

直接体绘制的shader位于DirectVolumeRenderingShader.shader文件中。它的绘制状态等如下:

	Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
	LOD 100
	Cull Front
	ZTest LEqual
	ZWrite On
	Blend SrcAlpha OneMinusSrcAlpha

此处Queue和RenderType均当做Transparent,使得先对不透明物体进行绘制,然后再绘制体渲染。顶点着色器函数为:

frag_in vert_main (vert_in v)
{
	frag_in o;
	o.vertex = UnityObjectToClipPos(v.vertex);
	o.uv = v.uv;
	o.vertexLocal = v.vertex;
	o.normal = UnityObjectToWorldNormal(v.normal);
	return o;
}

关键的实现位于frag_dvr函数中。该函数在开头首先确定了遍历步长,起始点,步长数量,遍历的方向。他们的计算如下:

#define NUM_STEPS 512
// 其中1.732f = sqrt(1+1+1),表示单位立方体对角线的长度,
// 插件直线的体绘制,会将volume数据塞到立方体中。最大不会超过立方体大小。
const float stepSize = 1.732f/*greatest distance in box*/ / NUM_STEPS;

// 由于pass开启了前表面剔除,那么下面公式计算的是后表面点,
// 然后将具体数值转换到了0到1的范围,用来和纹理坐标系的范围保持一致
// 这一步常规理解是放到步长采样结束之后,此处提前计算对于结果没有影响
float3 rayStartPos = i.vertexLocal + float3(0.5f, 0.5f, 0.5f); 

// ObjSpaceViewDir: 返回从给定对象空间顶点位置朝向摄像机的对象空间方向(未标准化)
// 这里认为,光线的方向都是从摄像机指向体数中心的方向(对象空间中)
float3 lightDir = normalize(ObjSpaceViewDir(float4(float3(0.0f, 0.0f, 0.0f), 0.0f)));
// 遍历的方向是从局部顶点指向相机的方向(对象空间中)
float3 rayDir = ObjSpaceViewDir(float4(i.vertexLocal, 0.0f));
rayDir = normalize(rayDir);

// Create a small random offset in order to remove artifacts
// 做随机扰动,减少体绘制锯齿
rayStartPos = rayStartPos + (2.0f * rayDir / NUM_STEPS) * tex2D(_NoiseTex, float2(i.uv.x, i.uv.y)).r;

问题一、此处采样步长是固定的,对于不同的体数据可能不适用。

接下来看一下具体遍历的逻辑(此处代码,不考虑cut,一维传输函数,光照的场景,删减后如下):

for (uint iStep = 0; iStep < NUM_STEPS; iStep++)
{
	const float t = iStep * stepSize;
	const float3 currPos = rayStartPos + rayDir * t;
	if (currPos.x < 0.0f || currPos.x >= 1.0f || currPos.y < 0.0f || currPos.y > 1.0f || currPos.z < 0.0f || currPos.z > 1.0f) // TODO: avoid branch?
		break;

	// Get the dansity/sample value of the current position
	const float density = getDensity(currPos);

	// Calculate gradient (needed for lighting and 2D transfer functions)
	// 梯度是通过纹理传入进来的
#if defined(TF2D_ON) || defined(LIGHTING_ON)
	float3 gradient = getGradient(currPos);
#endif

	float mag = length(gradient) / 1.75f;
	float4 src = getTF2DColour(density, mag);

	if (density < _MinVal || density > _MaxVal)
		src.a = 0.0f;

	col.rgb = src.a * src.rgb + (1.0f - src.a)*col.rgb;
	col.a = src.a + (1.0f - src.a)*col.a;

	if (src.a > 0.15f)
		iDepth = iStep;

	if (col.a > 1.0f)
		break;
}

问题二、 这里采用了从后往前采样混色的过程,这里有考虑了透明度大于1的时候提前终止遍历,这个判断条件通常是不起作用的,透明度的混色不会出现大于1的情况,需要选择合适的数值。

简单优化

从后往前采样混色,考虑了透明度体现终止;应该是需要从前往后进行遍历混色。因此,对DirectVolumeRenderingShader.shader进行细微调整,具体如下:

Shader "VolumeRendering/DirectVolumeRenderingShader"
{
    Properties
    {
		... // 保持一致
    }

    SubShader
    {
		...
        Cull Back // 前表面剔除修改为后表面剔除
        ...
        Pass
        {
	        ...
            frag_out frag_dvr (frag_in i)
            {
	            rayDir = -rayDir; // 采样方向取反
	            ...
	            for (uint iStep = 0; iStep < NUM_STEPS; iStep++)
                {
	                ...
		            col.rgb = col.rgb + (1-col.a)*src.rgb*src.a;
                    col.a = col.a + (1-col.a)*src.a;

                    if (src.a > 0.15f && iDepth != 0)
                        iDepth = iStep;

                    if (col.a > 0.95f)
                    {
                        //col = float4(1.0, 0.0, 0.0, 1.0);
                        break;
                    }    
                }
                ...
            }
        }

主要涉及到的修改有,

  • 采样方向更改,从前表面出发;
  • 混色方式更改,从前往后混色,和从后往前混色,混色实现是不一样的;
  • col.a > 0.95实现采样提前终止,增加绘制性能;
  • 深度获取,从前往后采样过程中,碰到第一个满足要求的位置,即为对应的深度;

如果将满足 col.a > 0.95该条件的内容进行绘制得到下图,可能还是有很大一部分可以满足提前终止的条件:

接着我们再来看一下优化前后的帧率对比,可以发现性能会有显著的提升。

优化前
优化后

参考

内置着色器 helper 函数 - Unity 手册

posted @ 2022-07-18 09:41  grassofsky  阅读(106)  评论(0编辑  收藏  举报