C for Graphic:geometry vertex to cube




     最近晚上在玩3ds路易鬼屋2暗月,其中路易进出鬼屋和实验室的效果有点意思,就是路易身体网格顶点变成立方体,然后穿越屏幕,跟老式科幻电影中人体变成数字信号一样,效果图如下:

        

     原本我以为这些效果都是美术做的particle animation,后面突然想到其实用geometryshader就可以实现(如果不知道geometry的同学可以返回之前看一下,有个大概的理解)。geometryshader属于桌面dx10的特性,当然vulkan dx都支持,以后嵌入式设备大面积使用vulkan的时候,嵌入式设备也可以用很多桌面图形库的特性(顺便说一下,好多设备都不支持opengl4.x版本,导致很多shader写法都用不了,各种报shader is not support on this device,这个也没办法)。一般情况下,我们使用unity编写shader,可编程函数就vertex/fragment/surface三个,而geometry则允许我们在vertex和fragment之间再操作顶点,更加灵活(想比如传统的vertex单纯的变换一下顶点的坐标,geometry则能操作点线面,包括坐标和增减等)。

     那么我们要想模仿路易鬼屋那样的穿越效果,则需要使用geometry将网格的顶点变换成立方体,比如一个顶点扩展出八个顶点组成一个cube,下面我们来shader实现一下。

     首先来一张示意图:

  

     简单明了,通过vertex扩展出v0-7的顶点然后建立网格即可,代码如下:

Shader "Custom/VertexToCubeShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _CubeWidth("Cube Width",Range(0,0.1)) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma target 4.0
            #pragma vertex vert
            #pragma fragment frag
            #pragma geometry geom
            
            #include "UnityCG.cginc"

            struct app2vert
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            }; 

            struct vert2geom
            {
                float4 vertex:POSITION;
                float2 uv:TEXCOORD0;
            };

            struct geom2frag
            {
                float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _CubeWidth;

            vert2geom vert (app2vert v)
            {
                vert2geom o;
                o.vertex = (v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            //拓扑顶点
            geom2frag topoVert(float4 vertex,float2 uv)
            {
                geom2frag gf;
                gf.vertex = vertex;
                gf.uv = uv;
                return gf;
            }

            //拓扑三角面
            void topoTri(float4 v0,float4 v1,float4 v2,float2 uv,inout TriangleStream tss)
            {
                tss.Append(topoVert(v0,uv));
                tss.Append(topoVert(v1,uv));
                tss.Append(topoVert(v2,uv));
            }
            
            //最大顶点数量的输入输出设置到满足拓扑需求
            [maxvertexcount(36)]
            void geom(triangle vert2geom vg[3],inout TriangleStream tss)
            {
				for(int i =0;i<3;i++)
				{
                    float4 localvertex = vg[i].vertex;
                    float2 uv = vg[i].uv;

                    float halfwid = _CubeWidth*0.5;

                    //构建立方体网格拓扑三角
                    float4 vertex0 = UnityObjectToClipPos(localvertex + float4(-halfwid,halfwid,-halfwid,0));
                    float4 vertex1 = UnityObjectToClipPos(localvertex + float4(halfwid,halfwid,-halfwid,0));
                    float4 vertex2 = UnityObjectToClipPos(localvertex + float4(halfwid,-halfwid,-halfwid,0));
                    float4 vertex3 = UnityObjectToClipPos(localvertex + float4(-halfwid,-halfwid,-halfwid,0));
                    float4 vertex4 = UnityObjectToClipPos(localvertex + float4(-halfwid,halfwid,halfwid,0));
                    float4 vertex5 = UnityObjectToClipPos(localvertex + float4(halfwid,halfwid,halfwid,0));
                    float4 vertex6 = UnityObjectToClipPos(localvertex + float4(halfwid,-halfwid,halfwid,0));
                    float4 vertex7 = UnityObjectToClipPos(localvertex + float4(-halfwid,-halfwid,halfwid,0));
                    
                    //添加拓扑关系
                    topoTri(vertex0,vertex1,vertex3,uv,tss);
                    topoTri(vertex1,vertex2,vertex3,uv,tss);
                    topoTri(vertex1,vertex2,vertex5,uv,tss);
                    topoTri(vertex2,vertex5,vertex6,uv,tss);
                    topoTri(vertex0,vertex4,vertex5,uv,tss);
                    topoTri(vertex0,vertex1,vertex5,uv,tss);
                    topoTri(vertex0,vertex3,vertex4,uv,tss);
                    topoTri(vertex3,vertex4,vertex7,uv,tss);
                    topoTri(vertex2,vertex3,vertex7,uv,tss);
                    topoTri(vertex2,vertex6,vertex7,uv,tss);
                    topoTri(vertex4,vertex6,vertex7,uv,tss);
                    topoTri(vertex4,vertex5,vertex6,uv,tss);

                    tss.RestartStrip();
				}
            }

            fixed4 frag (geom2frag i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

       效果图如下:

         

       原理就是拓扑出网格,如果还不太清楚网格拓扑含义的可以返回之前的网格构建看一下。

       最后我们需要仿照一下”穿越“效果,处理顶点移动,如下:

//将世界坐标转换成本地坐标去改变localvertex
                float4 localpasspoint = mul(unity_WorldToObject,_PassPoint);
				for(int i =0;i<3;i++)
				{
                    float4 localvertex = vg[i].vertex;
                    //这里特别注意,网格y值取之范围-0.5到0.5
                    //随着lerp从0-2插值(4s)
                    //加权的lerpwei则需要根据网格y值从0-1插值(2s)
                    //这样才能让网格y轴上顶点从上到下以此运动
                    if(vg[i].vertex.y>_PassThreshold)
                    {
                        float lerpwei = (vg[i].vertex.y-0.5)/(0.5-(-0.5));
                        localvertex = lerp(vg[i].vertex,localpasspoint,min(_PassLerp+lerpwei,1));
                    }

        这里我来解释一下:

        1.shader给美术调整坐标肯定是editor的世界坐标,所以要处理成建模坐标

        2.在建模坐标下,模型的顶点y值作为threshold阈值进行逐个移动

        3.使用lerp进行差值移动,注意需要根据顶点y值进行加权,让网格顶点从上到下依次移动

        顺便写个c#代码控制一下,如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;

public class VtoCMove : MonoBehaviour
{
    [Range(1f,10f)]
    [SerializeField] private float movetime;
    [Range(1f, 10f)]
    [SerializeField] private float thredtime;
    [SerializeField] private Material material;

    void Start()
    {
        
    }

    void OnGUI()
    {
        if(GUI.Button(new Rect(100,100,100,100),"move"))
        {
            material.DOFloat(2f, "_PassLerp", movetime);
            material.DOFloat(-0.5f, "_PassThreshold", thredtime);
        }
        if (GUI.Button(new Rect(100, 300, 100, 100), "restore"))
        {
            material.SetFloat("_PassLerp", 0f);
            material.SetFloat("_PassThreshold", 0.5f);
        }
    }
}

       效果图如下:

 

        不得不说任天堂的游戏就是这么有游戏性,想法都特别有意思,现在准备搞nds黄金太阳,过年打穿漆黑的黎明。

   

发布了103 篇原创文章 ·
获赞 117 ·
访问量 12万+