最近晚上在玩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黄金太阳,过年打穿漆黑的黎明。