切线空间可视化的两种方法——Geometry Shader和Mesh

目的:

  1. 实现任意模型三角面切线空间可视化,其他拓扑结构类推,顶点的切线空间也类似
  2. 类似方法也可以实现线框渲染
  3. Geometry Shader试验
  4. 说明下没事别瞎折腾,能偷懒就偷懒,偷懒往往效率高
  5. 为实现一种程式粒子效果做准备

实现环境:Unity(个人免费,白飘谁不飘)

效果如下:

看着有点恶心,:P,跟毛没刮干净一样,特别是着黑色时

左侧球,Geometry Shader

左侧正方体,Geometry Shader

右侧正方体,Mesh实现

右侧胶囊,Mesh实现

两个正方体作为对比,效果一样

Geometry Shader

核心想法是增加Geometry Shader接管几何化部分,改变原三角形几何结构,输出线段流。

与传统Vertex Shader输出后供Fragment Shader插值不同,当增加Geometry Shader后,Vertex Shader输出供Geometry Shader几何化,然后Geometry Shader再输出供Fragment Shader插值。Geometry Shader在这里的作用是改变(增删改都可以)几何(拓扑)结构。与此同时,数据流的定义也与以往不同,详细见下。

Vertex Shader

接收应用输入,包括顶点坐标vertex,切向量tangent,法向量normal,然后平推给Geometry Shader。这里假设都已经归一化过。这是让人不爽的地方,啥事都没干。

Geometry Shader

以输入三角形质心为原点,构建切线空间坐标轴,并给每个轴赋予不同颜色以示区别,把原本三角形输出变成线段流(也可以改成LineStrip,但着色不方便了,就好比Cube同一个点在不同面上为不同顶点一样)

质心为

float4 vCentroid = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3;

法线、切线全部相加后归一化(除不除3不影响归一化,假设输入是归一化后的)

fixed3 normal = normalize(IN[0].normal + IN[1].normal + IN[2].normal);

fixed3 tangent = normalize(IN[0].tangent + IN[1].tangent + IN[2].tangent);

副切线为切线和法线的叉积(归一化后叉积仍未单位向量),这里先不讲究左右手性

fixed3 bitagnent = cross(tangent, normal);

各轴上另取一点就可以构成坐标轴,例如切线上,其他轴类推

vCentroid + tangent * _ScaleTangent

构建线段。每根线段为2个(坐标一样颜色不一样的算不同点),并将坐标转换成裁剪空间坐标送入插值器,例如切线坐标轴,其他轴类推

o.color = _ColorTangent;

o1.pos = UnityObjectToClipPos(vCentroid + tangent * _ScaleTangent);

o1.color = _ColorTangent;

linetream.Append(o);

linetream.Append(o1);

linetream.RestartStrip();

至此就大功告成了。

Fragment Shader

直接输出插值点颜色就可以,线段两顶点同色,所以怎么插都是线段颜色。

详细代码:

BoreasGeometryTangentSpace.shader

BoreasEffectGeometryTriangle.cs,挂载到模型上就可以

详细见后附件

此方法讨论

  1. 相对简单,操作只在Geometry Shader,很直观,其他两个都在打酱油
  2. 低效,极其不推荐如此实现,这里只是为了一瞥Geometry Shader的样子。注意到,每帧都会重新构建各面的切线空间坐标轴,这种工作其实只需要做一次。于是就有了后面Mesh方法。

Mesh法

核心想法是遍历目标模型的每个三角面,直接构建一个新模型,原模型的每个面被线段构成的切线空间坐标轴替代。每次渲染完模型后,再渲染构建的新模型,从而只建模一次,效率极高。在这里,我们只关心模型,并不太关心材质相关,实际上只需对Mesh操作,并且是基于高层API,而不需要写Shader来操作,极大方便了划水。但对Mesh的数据结构要比较熟悉,操作一般都是基于索引。详细做法与Geometry Shader相同,需要注意的如下,

原模型三角形列表拿到的是顶点索引,而不是具体顶点,例如计算质心时如下,

int v0 = gameObjectMesh.triangles[i * 3];

int v1 = gameObjectMesh.triangles[i * 3 + 1];

int v2 = gameObjectMesh.triangles[i * 3 + 2];

Vector3 vertexOrigin = (gameObjectMesh.vertices[v0] + gameObjectMesh.vertices[v1] + gameObjectMesh.vertices[v2]) / 3.0f;

在构建坐标轴时,使用的是新模型的新索引,所以类似如下,

int idxVertexOrigin = vertices.Count;

lines.AddRange(new int[] {

idxVertexOrigin + 0, idxVertexOrigin + 1,

idxVertexOrigin + 2, idxVertexOrigin + 3,

idxVertexOrigin + 4, idxVertexOrigin + 5

});

详细代码:

BoreasGeometryTangentSpace.cs,建模+渲染

BoreasPointColor.shader,纯粹为了渲染,Unity自带Unlit/Color不支持逐顶点着色。

此方法讨论

  1. 效率高,只需一次建模。
  2. 对Mesh数据结构的了解要求稍微高点,示例未考虑多个subMesh的情况,如果需要支持多个subMesh,遍历所有subMesh。
  3. 运行时在场景里增加了模型实例,但问题也不大。Geometry Shader本质上也构建了一个新模型,还不能重复利用。

这个话题至此就介绍完了。本质上平平无奇,打法时间划下水

BoreasGeometryTangentSpace.shader

Shader "Boreas/Geometry/TangentSpace" {

    Properties {

        _ColorTangent ("Color of Tangent", Color) = (1, 0, 0, 1)

        _ScaleTangent ("Scale of Tangent", Range(0, 1)) = 0.1

        _ColorBitangent ("Color of Bitangent", Color) = (0, 1, 0, 1)

        _ScaleBitangent ("Scale of Bitangent", Range(0, 1)) = 0.1

        _ColorNormal ("Color of Normal", Color) = (0, 0, 1, 1)

        _ScaleNormal ("Scale of Normal", Range(0, 1)) = 0.1

    }

    SubShader

    {

        Tags { "RenderType"="Opaque" }

        LOD 100

        Pass {

            Name "TangentSpace"

            CGPROGRAM

            #pragma vertex vert

            #pragma geometry geom

            #pragma fragment frag

            #include "UnityCG.cginc"

            struct a2v {

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float3 tangent : TANGENT;

            };

            struct v2g {

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float3 tangent : TANGENT;

            };

            struct g2f {

                float4 pos : SV_POSITION;

                fixed4 color : COLOR;

            };

            fixed4 _ColorTangent;

            fixed _ScaleTangent;

            fixed4 _ColorBitangent;

            fixed _ScaleBitangent;

            fixed4 _ColorNormal;

            fixed _ScaleNormal;

            v2g vert (a2v v) {

                v2g o;

                o.vertex = v.vertex;

                o.normal = v.normal;

                o.tangent = v.tangent;

                return o;

            }

            [maxvertexcount(6)]

            void geom(triangle v2g IN[3], inout LineStream<g2f> linetream) {

                g2f o, o1;

                fixed3 normal = normalize(IN[0].normal + IN[1].normal + IN[2].normal);

                fixed3 tangent = normalize(IN[0].tangent + IN[1].tangent + IN[2].tangent);

                fixed3 bitagnent = cross(tangent, normal);

                float4 vCentroid = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3;

                vCentroid.xyz += normal * 0.02;

                o.pos = UnityObjectToClipPos(vCentroid);

                o.color = _ColorTangent;

                o1.pos = UnityObjectToClipPos(vCentroid + tangent * _ScaleTangent);

                o1.color = _ColorTangent;

                linetream.Append(o);

                linetream.Append(o1);

                linetream.RestartStrip();

                o.color = _ColorBitangent;

                o1.pos = UnityObjectToClipPos(vCentroid + bitagnent * _ScaleBitangent);

                o1.color = _ColorBitangent;

                linetream.Append(o);

                linetream.Append(o1);

                linetream.RestartStrip();

                o.color = _ColorNormal;

                o1.pos = UnityObjectToClipPos(vCentroid + normal * _ScaleNormal);

                o1.color = _ColorNormal;

                linetream.Append(o);

                linetream.Append(o1);

                linetream.RestartStrip();

            }

            fixed4 frag (g2f i) : SV_Target {

                return i.color;

            }

            ENDCG

        }

    }

}

BoreasEffectGeometryTriangle.cs

using UnityEngine;

using UnityEngine.Rendering;

namespace BoreasGame {

    [ExecuteAlways]

    public class BoreasEffectGeometryTriangle : MonoBehaviour {

        public Shader shader;

        Material material;

        CommandBuffer commandBuffer;

        void Start() {

            material = shader ? new Material(shader) : new Material(Shader.Find("Boreas/Geometry/TangentSpace"));

            commandBuffer = new CommandBuffer() {name = "BoreasEffectNormal"};

            foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>()) {

                for (int i = 0; i < renderer.sharedMaterials.Length; i++) {

                    commandBuffer.DrawRenderer(renderer, material, i, 0);

                }

            }

        }

        void OnRenderObject() {

            if (material != null && commandBuffer !=null) {

                Graphics.ExecuteCommandBuffer(commandBuffer);

            }

        }

    }

}

BoreasGeometryTangentSpace.cs

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Rendering;

namespace BoreasGame {

    public class BoreasGeometryTangentSpace : MonoBehaviour {

        public Color colorTangent = Color.red;

        [Range(0, 1)]

        public float scaleTangent = 0.1f;

        public Color colorBitangent = Color.green;

        [Range(0, 1)]

        public float scaleBitangent = 0.1f;

        public Color colorNormal = Color.blue;

        [Range(0, 1)]

        public float scaleNormal = 0.1f;

        Material material;

        CommandBuffer commandBuffer;

        GameObject effectObject;

        void Start() {

            CreateEffectObject();

            effectObject.transform.position = gameObject.transform.position;

            //material = new Material(Shader.Find("Unlit/Color"));

            material = new Material(Shader.Find("Boreas/PointColor"));

            commandBuffer = new CommandBuffer() {name = "BoreasEffectTangentSpace"};

            foreach (Renderer renderer in effectObject.GetComponentsInChildren<Renderer>()) {

                for (int i = 0; i < renderer.sharedMaterials.Length; i++) {

                    commandBuffer.DrawRenderer(renderer, material, i, 0);

                }

            }

        }

        void OnRenderObject() {

            if (material != null && commandBuffer !=null) {

                Graphics.ExecuteCommandBuffer(commandBuffer);

            }

        }

        void CreateEffectObject() {

            Mesh gameObjectMesh = gameObject.GetComponent<MeshFilter>().sharedMesh;

            effectObject = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer));

            Mesh mesh = new Mesh() {name = name};

            effectObject.GetComponent<MeshFilter>().sharedMesh = mesh;

            List<Vector3> vertices = new List<Vector3>();

            List<Color> colors = new List<Color>();

            List<int> lines = new List<int>();

            for (int i = 0; i < gameObjectMesh.triangles.Length / 3; i++) {

                int v0 = gameObjectMesh.triangles[i * 3];

                int v1 = gameObjectMesh.triangles[i * 3 + 1];

                int v2 = gameObjectMesh.triangles[i * 3 + 2];

                Vector3 tangent = (gameObjectMesh.tangents[v0] + gameObjectMesh.tangents[v1] + gameObjectMesh.tangents[v2]).normalized;

                Vector3 normal = (gameObjectMesh.normals[v0] + gameObjectMesh.normals[v1] + gameObjectMesh.normals[v2]).normalized;

                Vector3 bitangent = Vector3.Cross(tangent, normal);

                Vector3 vertexOrigin = (gameObjectMesh.vertices[v0] + gameObjectMesh.vertices[v1] + gameObjectMesh.vertices[v2]) / 3.0f;

                vertexOrigin += normal * 0.02f;

                Vector3 vertexTangent = vertexOrigin + tangent * scaleTangent;

                Vector3 vertexBitangent = vertexOrigin + bitangent * scaleBitangent;

                Vector3 vertexNormal = vertexOrigin + normal * scaleNormal;

                int idxVertexOrigin = vertices.Count;

                vertices.Add(vertexOrigin);         colors.Add(colorTangent);

                vertices.Add(vertexTangent);        colors.Add(colorTangent);

                vertices.Add(vertexOrigin);         colors.Add(colorBitangent);

                vertices.Add(vertexBitangent);      colors.Add(colorBitangent);

                vertices.Add(vertexOrigin);         colors.Add(colorNormal);

                vertices.Add(vertexNormal);         colors.Add(colorNormal);

                lines.AddRange(new int[] {

                    idxVertexOrigin + 0, idxVertexOrigin + 1,

                    idxVertexOrigin + 2, idxVertexOrigin + 3,

                    idxVertexOrigin + 4, idxVertexOrigin + 5

                });

            }

            mesh.SetVertices(vertices);

            mesh.SetColors(colors);

            mesh.SetIndices(lines, MeshTopology.Lines, 0);

        }

    }

}

BoreasPointColor.shader

Shader "Boreas/PointColor" {

    Properties {

    }

    SubShader {

        Tags { "RenderType"="Opaque" }

        LOD 100

        Pass {

            Name "PointColor"

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include "UnityCG.cginc"

            struct a2v {

                float4 vertex : POSITION;

                fixed4 color : COLOR;

            };

            struct v2f {

                float4 pos : SV_POSITION;

                fixed4 color : COLOR;

            };

            v2f vert (a2v v) {

                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);

                o.color = v.color;

                return o;

            }

            fixed4 frag (v2f i) : SV_Target {

                return i.color;

            }

            ENDCG

        }

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值