前言
之前看过米哈游大佬制作的桃源恋歌MMD,被其中的Mesh变换转场效果所折服了,所以自己想模仿着实现这个效果,幸好kerjiro技术美术大神开源了这方面的视觉特效项目,感觉自己如果想成为TA还有好长的路要走。。。
最终效果
首先让我们来看一下最终效果
实现思路
其实这个效果是通过几何着色器来实现的,主要思路就是通过几何着色器对三角面片的顶点进行添加,构成一个Cube。
几何着色器
相信大家接触的最多的应该是顶点着色器和像素着色器,那么什么是几何着色器呢?
定义
在顶点和片段着色器之间有一个可选的着色器,叫做几何着色器(Geometry Shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)即图元的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。
使用几何着色器进行图元转换
声明着色器
#pragma geometry geom
设置输出顶点数量,其中N为几何着色器为单个调用输出的顶点最大数量,几何着色器每次输出的顶点数量是可变的,但是不能超过定义的最大值, 出于性能考虑,最大顶点数应尽可能小; 当GS输出在1到20个标量之间时,可以实现GS的性能峰值,如果GS输出在27-40个标量之间,则性能下降50%。每次调用的标量输出数是最大顶点输出数和输出顶点类型结构中的标量数的乘积。
[maxvertexcount(N)]
声明输入和输出的Struct
//传递给几何着色器的数据
struct v2g
{
float4 vertex:POSITION;
float3 normal:TEXCOORD0;
//float2 uv:TEXCOORD1;
};
//传递给像素着色器的数据
struct g2f
{
float4 pos : SV_POSITION;
float3 normal:TEXCOORD0;
//float2 uv : TEXCOORD1;
float4 color:COLOR;
};
设置几何着色器输入参数和输出参数,
其中“triangle”为输入的图元类型, 输入参数一定为顶点数组 。
输入图元类型 | 所需顶点数
-|-
point | 输入1个点的1个顶点
line | 输入1条直线的2个顶点
lineadj | 输入1条具有邻接(lists或strips)的线段的4个顶点
triangle | 输入1个三角形的3个顶点
triangleadj | 输入1个具有邻接(lists或strips)的三角形的6个顶点
TriangleStream为流类型(stream type)对象,还有 LineStream 和 PointStream ,存储着由几何着色器输出的几何体顶点列表。内置Append用于向输出流添加顶点序列, 若想扩展输入的图元,也可以用内置Append向输出流添加多出来的顶点。
当指定uint primID:SV_PrimitiveID时,输入汇编阶段会为每个图元自动生成一个图元ID。当调用draw方法绘制n个图元时,ID号为0到n-1,这里用到的原因是为了随机Cube化。
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
{
//shader body
}
将输出顶点传送至输出stream上
OutputStream.Append(o);
开始动手
CPU传递数据给Shader
首先我们需要新建一个脚本用于传递数据给Shader,以下就是K神的传递脚本,我已经写好注释。挂在一个空物体上面即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//在编辑器模式下也可运行
[ExecuteInEditMode]
public class Voxelizer : MonoBehaviour
{
//SerializeField用于面板上显示非Public的参数
//Range(0,1)控制可滑选的范围
//控制生成方块的密度
[SerializeField, Range(0, 1)] float _density = 0.05f;
//控制生成方块的大小
[SerializeField, Range(0, 10)] float _scale = 3;
//动画参数
//用于控制方块变形后的长度
[SerializeField, Range(0, 20)] float _stretch = 5;
//用于控制方块变形后上升的最大距离
[SerializeField, Range(0, 1000)] float _fallDistance = 1;
//用于控制方块变形后的随机移动范围
[SerializeField, Range(0, 10)] float _fluctuation = 1;
//颜色参数
[SerializeField, ColorUsage(false, true)] Color _emissionColor1 = Color.black;
[SerializeField, ColorUsage(false, true)] Color _emissionColor2 = Color.black;
[SerializeField, ColorUsage(false, true)] Color _transitionColor = Color.white;
[SerializeField, ColorUsage(false, true)] Color _lineColor = Color.white;
//用于Mesh变换物体的Renderer
[SerializeField] Renderer[] _renderers = null;
//效果平面的位置与距离
Vector4 EffectorPlane
{
get
{
//获取向前的方向
var fwd = transform.forward / transform.localScale.z;
//获取向前方向上的移动距离
var dist = Vector3.Dot(fwd, transform.position);
return new Vector4(fwd.x, fwd.y, fwd.z, dist);
}
}
//将RGB颜色模型转为HSV颜色模型
Vector4 ColorToHsvm(Color color)
{
//获取颜色的分量最大值
var max = color.maxColorComponent;
float h, s, v;
Color.RGBToHSV(color / max, out h, out s, out v);
return new Vector4(h, s, v, max);
}
//获取着色器属性的唯一标识符
//优点:使用属性标识符比将字符串传递给所有材料属性函数更有效。
//例如,如果您经常调用Material.SetColor或使用MaterialPropertyBlock,
//则最好只获取一次所需属性的标识符。
static class ShaderIDs
{
public static readonly int VoxelParams = Shader.PropertyToID("_VoxelParams");
public static readonly int AnimParams = Shader.PropertyToID("_AnimParams");
public static readonly int EmissionHsvm1 = Shader.PropertyToID("_EmissionHsvm1");
public static readonly int EmissionHsvm2 = Shader.PropertyToID("_EmissionHsvm2");
public static readonly int TransitionColor = Shader.PropertyToID("_TransitionColor");
public static readonly int LineColor = Shader.PropertyToID("_LineColor");
public static readonly int EffectorPlane = Shader.PropertyToID("_EffectorPlane");
public static readonly int PrevEffectorPlane = Shader.PropertyToID("_PrevEffectorPlane");
public static readonly int LocalTime = Shader.PropertyToID("_LocalTime");
}
//在要使用相同材质但属性稍有不同的多个对象绘制的情况下使用MaterialPropertyBlock。
MaterialPropertyBlock _sheet;
Vector4 _prevEffectorPlane = Vector3.one * 1e+5f;
private void LateUpdate()
{
//查看渲染列表是否为空
if (_renderers == null || _renderers.Length == 0) return;
//创建新的MaterialPropertyBlock
if (_sheet == null) _sheet = new MaterialPropertyBlock();
var plane = EffectorPlane;
// Filter out large deltas.
//过滤掉大的三角面片
if ((_prevEffectorPlane - plane).magnitude > 100) _prevEffectorPlane = plane;
//存储参数
var vparams = new Vector2(_density, _scale);
var aparams = new Vector3(_stretch, _fallDistance, _fluctuation);
var emission1 = ColorToHsvm(_emissionColor1);
var emission2 = ColorToHsvm(_emissionColor2);
//将参数传递给shader
foreach (var renderer in _renderers)
{
if (renderer == null) continue;
renderer.GetPropertyBlock(_sheet);
_sheet.SetVector(ShaderIDs.VoxelParams, vparams);
_sheet.SetVector(ShaderIDs.AnimParams, aparams);
_sheet.SetVector(ShaderIDs.EmissionHsvm1, emission1);
_sheet.SetVector(ShaderIDs.EmissionHsvm2, emission2);
_sheet.SetColor(ShaderIDs.TransitionColor, _transitionColor);
_sheet.SetColor(ShaderIDs.LineColor, _lineColor);
_sheet.SetVector(ShaderIDs.EffectorPlane, plane);
_sheet.SetVector(ShaderIDs.PrevEffectorPlane, _prevEffectorPlane);
//_sheet.SetFloat(ShaderIDs.LocalTime, time);
renderer.SetPropertyBlock(_sheet);
print(plane);
}
}
//进行gizmo编辑器的实现,用于可视化Debug
Mesh _gridMesh;
void OnDestroy()
{
if (_gridMesh != null)
{
if (Application.isPlaying)
Destroy(_gridMesh);
else
DestroyImmediate(_gridMesh);
}
}
void OnDrawGizmos()
{
if (_gridMesh == null) InitGridMesh();
//矩阵用于控制Gizmos跟随物体的移动而移动
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = new Color(1, 1, 0, 0.5f);
Gizmos.DrawWireMesh(_gridMesh, Vector3.zero);
Gizmos.DrawWireMesh(_gridMesh, Vector3.forward);
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawWireCube(Vector3.forward / 2, new Vector3(0.02f, 0.02f, 1));
}
void InitGridMesh()
{
const float ext = 0.5f;
const int columns = 10;
var vertices = new List<Vector3>();
var indices = new List<int>();
for (var i = 0; i < columns + 1; i++)
{
var x = ext * (2.0f * i / columns - 1);
indices.Add(vertices.Count);
vertices.Add(new Vector3(x, -ext, 0));
indices.Add(vertices.Count);
vertices.Add(new Vector3(x, +ext, 0));
indices.Add(vertices.Count);
vertices.Add(new Vector3(-ext, x, 0));
indices.Add(vertices.Count);
vertices.Add(new Vector3(+ext, x, 0));
}
_gridMesh = new Mesh { hideFlags = HideFlags.DontSave };
_gridMesh.SetVertices(vertices);
_gridMesh.SetNormals(vertices);
_gridMesh.SetIndices(indices.ToArray(), MeshTopology.Lines, 0);
_gridMesh.UploadMeshData(true);
}
}
MaterialPropertyBlock
研究代码的时候发现了MaterialPropertyBlock,查阅文档才发现是用于节约性能。实际应用可以查看这篇文章MaterialPropertyBlock。
Shader的实现
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/MeshShader"
{
Properties
{
_MainTex("主纹理贴图",2D)="white"{}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
//声明着色器
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
#include "Assets/My/SimplexNoise3D.hlsl"
//传递给顶点着色器的数据
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
//传递给几何着色器的数据
struct v2g
{
float4 vertex:POSITION;
float3 normal:TEXCOORD0;
//float2 uv:TEXCOORD1;
};
//传递给像素着色器的数据
struct g2f
{
float4 pos : SV_POSITION;
float3 normal:TEXCOORD0;
//float2 uv : TEXCOORD1;
float4 color:COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
//用于几何着色器的数据
half2 _VoxelParams; // density, scale 密度,比例
half3 _AnimParams; // stretch, fall distance, fluctuation 伸展、下降距离、波动
float4 _EffectorPlane;
float4 _PrevEffectorPlane;
//用于像素着色器的数据
half4 _EmissionHsvm1;
half4 _EmissionHsvm2;
half3 _TransitionColor;
half3 _LineColor;
//顶点着色器
void vert(inout v2g input)
{
}
g2f VertexOutput(
float3 position0, float3 position1,
half3 normal0, half3 normal1, half param,
half emission = 0, half random = 0, half2 baryCoord = 0.5
)
{
g2f i;
i.pos = UnityObjectToClipPos(float4(lerp(position0, position1, param),1));
i.normal = normalize(lerp(normal0, normal1, param));
i.color = float4(baryCoord, emission,random);
return i;
}
// 计算方块的位置和大小
void CubePosScale(
float3 center, float size, float rand, float param,
out float3 pos, out float3 scale
)
{
const float VoxelScale = _VoxelParams.y;
const float Stretch = _AnimParams.x;
const float FallDist = _AnimParams.y;
const float Fluctuation = _AnimParams.z;
// Noise field
//噪声场
float4 snoise = snoise_grad(float3(rand * 2378.34, param * 0.8, 0));
// Stretch/move param
float move = saturate(param * 4 - 3);
move = move * move;
// Cube position
pos = center + snoise.xyz * size * Fluctuation;
pos.y += move * move * lerp(0.25, 1, rand) * size * FallDist;
// Cube scale anim
scale = float2(1 - move, 1 + move * Stretch).xyx;
scale *= size * VoxelScale * saturate(1 + snoise.w * 2);
}
//哈希值,用于随机觉得面片是三角面片还是Cube
float Hash(uint s)
{
s = s ^ 2747636419u;
s = s * 2654435769u;
s = s ^ (s >> 16);
s = s * 2654435769u;
s = s ^ (s >> 16);
s = s * 2654435769u;
return float(s) * rcp(4294967296.0); // 2^-32
}
//几何着色器
[maxvertexcount(24)]
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
{
//获取密度
const float VoxelDensity = _VoxelParams.x;
//获取传入顶点的位置
float3 p0 = input[0].vertex.xyz;
float3 p1 = input[1].vertex.xyz;
float3 p2 = input[2].vertex.xyz;
float3 p0_prev = p0;
float3 p1_prev = p1;
float3 p2_prev = p2;
//获取传入顶点的法线
float3 n0 = input[0].normal;
float3 n1 = input[1].normal;
float3 n2 = input[2].normal;
//计算中心点
float3 center = (p0 + p1 + p2) / 3;
float size = distance(p0, center);
//变形参数
//将中心点变换到世界空间中
float3 center_ws = mul(unity_ObjectToWorld, float4(center,1)).xyz;
float param = 1 - dot(_EffectorPlane.xyz, center_ws) + _EffectorPlane.w;
//如果变形还没开始那就将平常操作
if (param < 0)
{
outStream.Append(VertexOutput(p0, 0, n0, 0, 0, 0, 0));
outStream.Append(VertexOutput(p1, 0, n1, 0, 0, 0, 0));
outStream.Append(VertexOutput(p2, 0, n2, 0, 0, 0, 0));
outStream.RestartStrip();
return;
}
//变形结束后,不传递任何数据,从而使物体隐身
if (param >= 1) return;
// Choose cube/triangle randomly.
//uint seed = float3(pid * 877, pid * 877, pid * 877);
uint seed = pid * 877;
if (Hash(seed) < VoxelDensity)
{
// -- Cube --
// Random numbers
float rand1 = Hash(seed + 1);
float rand2 = Hash(seed + 5);
// Cube position and scale
float3 pos, pos_prev, scale, scale_prev;
CubePosScale(center, size, rand1, param, pos, scale);
// Secondary animation parameters
float morph = smoothstep(0, 0.25, param);
float em = smoothstep(0, 0.15, param) * 2; // initial emission
em = min(em, 1 + smoothstep(0.8, 0.9, 1 - param));
em += smoothstep(0.75, 1, param); // emission while falling
// Cube points calculation
float3 pc0 = pos + float3(-1, -1, -1) * scale;
float3 pc1 = pos + float3(+1, -1, -1) * scale;
float3 pc2 = pos + float3(-1, +1, -1) * scale;
float3 pc3 = pos + float3(+1, +1, -1) * scale;
float3 pc4 = pos + float3(-1, -1, +1) * scale;
float3 pc5 = pos + float3(+1, -1, +1) * scale;
float3 pc6 = pos + float3(-1, +1, +1) * scale;
float3 pc7 = pos + float3(+1, +1, +1) * scale;
// World space to object space conversion
// Vertex outputs
float3 nc = float3(-1, 0, 0);
outStream.Append(VertexOutput(p0, pc2, n0, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p2, pc0, n2, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p0, pc6, n0, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p2, pc4, n2, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
nc = float3(1, 0, 0);
outStream.Append(VertexOutput(p2, pc1, n2, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p1, pc3, n1, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p2, pc5, n2, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p1, pc7, n1, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
nc = float3(0, -1, 0);
outStream.Append(VertexOutput(p2, pc0, n2, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p2, pc1, n2, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p2, pc4, n2, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p2, pc5, n2, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
nc = float3(0, 1, 0);
outStream.Append(VertexOutput(p1, pc3, n1, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p0, pc2, n0, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p1, pc7, n1, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p0, pc6, n0, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
nc = float3(0, 0, -1);
outStream.Append(VertexOutput(p2, pc1, n2, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p2, pc0, n2, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p1, pc3, n1, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p0, pc2, n0, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
nc = float3(0, 0, 1);
outStream.Append(VertexOutput(p2, pc4, -n2, nc, morph, em, rand2, float2(0, 0)));
outStream.Append(VertexOutput(p2, pc5, -n2, nc, morph, em, rand2, float2(1, 0)));
outStream.Append(VertexOutput(p0, pc6, -n0, nc, morph, em, rand2, float2(0, 1)));
outStream.Append(VertexOutput(p1, pc7, -n1, nc, morph, em, rand2, float2(1, 1)));
outStream.RestartStrip();
}
else
{
// -- Triangle --
half morph = smoothstep(0, 0.25, param);
//half morph = 0.25;
half em = smoothstep(0, 0.15, param) * 2;
outStream.Append(VertexOutput(p0, center, n0, n0, morph, em));
outStream.Append(VertexOutput(p1, center, n1, n1, morph, em));
outStream.Append(VertexOutput(p2, center, n2, n2, morph, em));
outStream.RestartStrip();
}
}
//计算颜色
half3 SelfEmission(g2f input)
{
half2 bcc = input.color.rg;
half em1 = saturate(input.color.b);
half em2 = saturate(input.color.b - 1);
half rand = input.color.a;
// Cube face color
half3 face = lerp(_EmissionHsvm1.xyz, _EmissionHsvm2.xyz, rand);
face *= lerp(_EmissionHsvm1.w, _EmissionHsvm2.w, rand);
// Cube face attenuation
face *= lerp(0.75, 1, smoothstep(0, 0.5, length(bcc - 0.5)));
// Edge detection
half2 fw = fwidth(bcc);
half2 edge2 = min(smoothstep(0, fw * 2, bcc),
smoothstep(0, fw * 2, 1 - bcc));
half edge = 1 - min(edge2.x, edge2.y);
return
face * em1 +
_TransitionColor * em2 * face +
edge * _LineColor * em1;
}
half4 frag(g2f z):SV_Target
{
half4 col = half4(SelfEmission(z),1);
return col;
}
ENDCG
}
}
FallBack "Diffuse"
}
这里运用到了三维噪声的知识,这里我只是简单的调用了K神写好的噪声函数,并没有深究,其实我还是看过了一些关于噪声的文章,以后已机会把笔记总结出来。
最终结果
注意
这个效果对于模型也是有要求的,模型的面片不能太小,不然就会得到以下的结果…