C for Graphic:3D Printing

        前几天同事问我怎么实现3d打印效果,具体就是一个模型从底部到顶部显现。

        那么这个问题的核心就是模型的“裁剪”,通常做法就是根据网格顶点的建模空间(或者世界空间)坐标进行判断(一般通过Y的值)是否discard,实现如下:

Shader "3DPrint/3DPrintVertexShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _VertexClip("Vertex Clip",vector) = (0,0,0,0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Off
            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;
                float4 worldvertex : TEXCOORD1;
                float4 localvertex : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float4 _VertexClip;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldvertex = mul(UNITY_MATRIX_M,v.vertex);
                o.localvertex = v.vertex;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                if(i.localvertex.y>_VertexClip.y)      //如果localvertex.y大于坐标y的裁剪值,就discard
                //if(i.worldvertex.y>_VertexClip.y)      //worldvertex.y大于坐标y的裁剪值,就discard
                {
                    discard;
                }
                return col;
            }
            ENDCG
        }
    }
}

        效果如下:

         还有一种方法通过Mask操作,将Sphere需要裁剪的部分通过Mask裁剪掉,使用ColorMask,解释如下:

ColorMask RGB | A | 0 | any combination of R, G, B, A

Set color channel writing mask. Writing ColorMask 0 turns off rendering to all color channels. Default mode is writing to all channels (RGBA), but for some special effects you might want to leave certain channels unmodified, or disable color writes completely.

When using multiple render target (MRT) rendering, it is possible to set up different color masks for each render target, by adding index (0–7) at the end. For example, ColorMask RGB 3 would make render target #3 write only to RGB channels.

         颜色遮罩,就是原本shader中frag函数返回计算后的Color.rgb,被ColorMask命令关闭RGBA四个Channel的渲染(写入帧缓冲区),那么表现效果就是继续渲染帧(颜色)缓冲区中已经存在的pixels。如果使用Defferd Rendering的MRT,则可以根据index0-7选择返回RT的pixels,延迟渲染我们之前聊过了,不明白的同学可以返回了解。

         那么我们需要先写一个渲染队列最小(也就是最先渲染)的Skybox作为初始帧缓冲区的pixels

Shader "3DPrint/3DSkyboxShader"
{
    Properties
    {
        _CubeTex ("Cube Tex", CUBE) = "" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="background"}  //background = 1000
        LOD 100
        Cull Off

        Pass
        {
            ZWrite On
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float3 refl : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            samplerCUBE _CubeTex;

            float3 reflectEx(float3 inLight,float3 norm)
			{
				return inLight -2.0*norm*dot(norm,inLight);
			}

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float3 inlight = -normalize(WorldSpaceViewDir(v.vertex));
                float3 worldnormal = normalize(UnityObjectToWorldNormal(v.normal));
                o.refl = reflectEx(inlight,worldnormal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = texCUBE(_CubeTex, i.refl);
                return col;
            }
            ENDCG
        }
    }
}

          再使用ColorMask写一个渲染队列高于Skybox10个层级的ClearMask shader,用于“相交遮罩”效果。

Shader "3DPrint/3DClearShader"
{
    SubShader 
    {
        Tags {"RenderType"="Opaque" "Queue"="background+10"}   //background = 1000
        
        ColorMask 0

        Pass {}
    }
}

           接着将3D Clear Material赋予一个Cube,通过Cube与Sphere相交,产生效果如下:

        是不是看出什么问题了,Scene和Game表现效果不一致,在Scene中按照我们构思的效果正常表示,而在Game中:

        在Camera.ClearFlags选择Skybox下,用于ColorMask的Cube表现为纯黑色,也就意味着Cube颜色渲染之前的帧缓冲区pixels为纯黑色,而将Camera.ClearFlags改为SolidColor,则表现正常,也就是帧缓冲区变为了正常的SolidColor。

        这时候我切换了Deffered Rendering模式,同时特意测试添加一张背景图,专门用于帧缓冲区的填充。

         可以看得出来Game下效果就正常了,背景doge图被填充成为帧缓冲区pixels,所以Cube用于ColorMask的效果就表现正常了,那么这样就意味着Scene和Game在处理Skybox的渲染不一致,而Game下会造成我们写一些特殊效果的困扰。我具体不明白这算是unity的bug还是特意为之,但确实造成了问题。      

         当然了这种ColorMask的方式也达不到我们需要的3D打印效果,因为这属于相交遮罩,不属于裁剪,所以我们也不用。

         这么一看第一种方法就挺好用,但是呢!结合项目做3d打印效果功能,光靠这个shader去做不现实,因为如果你要裁剪的是一个很多SubMesh和SubMaterial的组合体(大概率都是这类模型),你不可能把所有美术给你的模型的材质shader改一遍(当然非要刚一下,我就要全改一遍,把整个项目shader都加上裁剪功能)。这么一想,想做个比较通用的3D Print效果还挺麻烦的,当然我想到了解决方案。

         1.首先我们合并SubMesh为一个ClipMesh备用

         2.写一个“裁剪”Shader作用于ClipMesh,制作顶点裁剪效果

         3.使用CommandBuffer渲染原始的SubMesh和SubMaterial到RenderTexture,配合“裁剪Shader”做渲染

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SubMeshCombine : MonoBehaviour
{
    public GameObject[] Gos;
    public Material clipMat;
    private MeshRenderer meshRender;
    private MeshFilter meshFilter;

    void Start()
    {
        meshRender = gameObject.AddComponent<MeshRenderer>();
        meshRender.sharedMaterial = clipMat;
        meshFilter = gameObject.AddComponent<MeshFilter>();

        Combine();
    }

    private void Combine()
    {
        Mesh cbmesh = new Mesh();
        int cblen = Gos.Length;
        CombineInstance[] cbinsts = new CombineInstance[cblen];
        for (int i = 0; i < cblen; i++)
        {
            GameObject go = Gos[i];
            MeshFilter filter = go.GetComponent<MeshFilter>();
            CombineInstance cbinst = new CombineInstance
            {
                mesh = filter.sharedMesh,
                transform = go.transform.localToWorldMatrix,
            };
            cbinsts[i] = cbinst;
            go.SetActive(false);
        }
        cbmesh.CombineMeshes(cbinsts);
        meshFilter.sharedMesh = cbmesh;
    }
}

 

Shader "3DPrint/ClipVertexShader"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
        _LightFactor("Light Factor",Color) = (1,1,1,1)
        _DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
        _ClipPosY("Clip Pos Y",float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 localvertex : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldP2S : TEXCOORD2;
            };

            float4 _MainColor;
            float _ClipPosY;

            float4 _LightFactor;
            float4 _DiffuseFactor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.localvertex = v.vertex;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldP2S = WorldSpaceLightDir(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //处理vert到frag插值后的normalize
                float3 uniformWorldNormal = normalize(i.worldNormal);
                float3 uniformWorldP2S = normalize(i.worldP2S);
                fixed4 col = _MainColor;
                fixed3 light = _LightColor0.rgb * _LightFactor;
                fixed3 diffuse = _LightColor0.rgb * max(dot(uniformWorldNormal,uniformWorldP2S),0) * _DiffuseFactor;
                col *= fixed4(light+diffuse,1);
                if(i.localvertex.y>_ClipPosY)
                {
                    discard;
                }
                return col;
            }
            ENDCG
        }
    }
}

         c#代码为合并网格,shader代码就是一个简单光照+顶点裁剪,效果如下:

         我们将SubMesh合并到一个ClipMesh,然后我们就可以独立操作新的ClipMesh了。而Shader效果就是基本的顶点裁剪,同时我们看的出来,渲染管线vert函数到frag函数进行了顶点插值,所以“裁剪”的步长也很平滑。

         “裁剪”效果出来了,接下来就是处理渲染效果。

         首先将原始渲染效果提取到RT上:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CameraClipEffect : MonoBehaviour
{
    public GameObject[] cmdObjs;
    public Material clipMat;
    private CommandBuffer cmdBuffer;
    private RenderTexture cmdRT;
    void Start()
    {

    }

    void OnEnable()
    {
        cmdRT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32);
        cmdBuffer = new CommandBuffer();
        cmdBuffer.SetRenderTarget(cmdRT);
        cmdBuffer.ClearRenderTarget(true, true, Color.black);
        for (int i = 0; i < cmdObjs.Length; i++)
        {
            GameObject go = cmdObjs[i];
            MeshRenderer render = go.GetComponent<MeshRenderer>();
            cmdBuffer.DrawRenderer(render, render.sharedMaterial);
        }
        Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmdBuffer);
    }

    void OnDisable()
    {
        RenderTexture.ReleaseTemporary(cmdRT);
        cmdRT = null;

        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, cmdBuffer);
        cmdBuffer.Clear();
        cmdBuffer = null;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (cmdBuffer != null)
        {
            Graphics.ExecuteCommandBuffer(cmdBuffer);
        }

        Graphics.Blit(cmdRT, destination);
    }
}

        效果如下:

 

         接下来要混合这张RT渲染+裁剪效果:

         首先修改ClipVertexShader,我们要添加获取cmdRT,然后进行屏幕采样映射:

Shader "3DPrint/ClipVertexWhtiRTShader"
{
    Properties
    {
        _CmdRT("Command RenderTexture",2D) = "white" {}
        _ClipPosY("Clip Pos Y",float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 localvertex : TEXCOORD1;
                float4 screenpos : TEXCOORD2;
            };

            sampler2D _CmdRT;
            float _ClipPosY;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.localvertex = v.vertex;
                o.screenpos = ComputeScreenPos(o.vertex);       
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //因为RT是屏幕采样,所以我们映射纹理要使用屏幕坐标
                fixed4 col = tex2D(_CmdRT,float2(i.screenpos.xy/i.screenpos.w));    
                if(i.localvertex.y>_ClipPosY)
                {
                    discard;
                }
                return col;
            }
            ENDCG
        }
    }
}

         然后修改CameraClipEffect,功能修改为只提取cmdRT然后赋于新的ClipVertexWithRT shader:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CameraClipEffect : MonoBehaviour
{
    public GameObject[] cmdObjs;
    public Material clipMat;
    private CommandBuffer cmdBuffer;
    private RenderTexture cmdRT;
    void Start()
    {

    }

    void OnEnable()
    {
        cmdRT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32);
        cmdBuffer = new CommandBuffer();
        cmdBuffer.SetRenderTarget(cmdRT);
        cmdBuffer.ClearRenderTarget(true, true, Color.black);
        for (int i = 0; i < cmdObjs.Length; i++)
        {
            GameObject go = cmdObjs[i];
            MeshRenderer render = go.GetComponent<MeshRenderer>();
            cmdBuffer.DrawRenderer(render, render.sharedMaterial);
        }
        Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmdBuffer);
        clipMat.SetTexture("_CmdRT", cmdRT);
    }

    void OnDisable()
    {
        RenderTexture.ReleaseTemporary(cmdRT);
        cmdRT = null;

        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, cmdBuffer);
        cmdBuffer.Clear();
        cmdBuffer = null;
    }

    // private void OnRenderImage(RenderTexture source, RenderTexture destination)
    // {
    //     if (cmdBuffer != null)
    //     {
    //         Graphics.ExecuteCommandBuffer(cmdBuffer);
    //     }

    //     Graphics.Blit(cmdRT, destination);
    // }
}

        最后我们运行看效果:

       这里我说一下要注意的一点:

       我们采样RT是基于屏幕坐标,那么我们将RT赋予合并后的mesh,并不能直接使用,而是需要使用ComputeScreenPos,取到Vertex的屏幕坐标(齐次坐标),然后采样使用xy/w(也就叫透视除法)取得正确的uv。不明白ComputeScreenPos的同学可以返回我之前博客了解,这里再次贴上官方:

      

       unity官方shader API

       这样我们就理想的解决了3D Printing,也就是3D打印效果的实现,当然了如果有小伙伴嫌这个效果不够漂亮,可以自己额外添加着色算法。

Guess you like

Origin blog.csdn.net/yinhun2012/article/details/108857010