Unity引擎制作万人同屏效果
大家好,我是阿赵。
之前分析了多人同屏的一些思路。在我的思路里面,把角色骨骼动画转换成顶点动画是一个比较中心的思想。
一、资源分析
比如我这里有一只狼的模型:
它的身上挂着Animator,通过骨骼动画运动
二、 VAT实现
当我们有了这些资源之后,如果可以把它们烘焙一下顶点,每一帧记录一下它们的顶点数据,是不是就可以在播放的时候,每一帧改变模型网格的顶点,达到顶点动画的效果呢?
不过实际上要记录的东西很多,如果说,这个模型的网格有1000个顶点,一个动作有200帧,那么需要记录的顶点数据就有1000200个这么多了。不过看1000200这个东西时,我们是否可以联想到另外一样东西呢?
没错,看起来1000200好像是一个很大的数字,但实际上如果是一张1000200的图片,我们并不会觉得它很大。然后一个顶点有x、y、z三个轴的坐标需要记录,一个像素的颜色同样有rgb三个值可以使用。那么接下来的事情就变得简单了,我们可以使用一张贴图来记录一个动作的所有帧的顶点坐标。
比如这样:
这样一张贴图,x坐标代表的是第几个顶点,y坐标代表的是第几帧,所以对应的某个像素点代表的就是第几个顶点在第几帧的坐标。
这个通过贴图记录顶点动画的技术,就是叫做Vertex Animation Texture(顶点动画贴图),简称VAT。
要得到这样的一张贴图,我们需要做的事情有:
- 要展一套UV2,让每个顶点的VU2的x坐标按顺序排列
- 控制动画,每一帧停顿一次,然后把每个顶点的位置记录下来。
具体的生成步骤,会在下一篇里面说明。这篇里面主要是说明一下,假设我们有了这一个VAT贴图之后,应该怎样去播放。
三、 播放
在刚才的贴图里面,实际上有一些数据是没有表达清楚的。比如说,一个动作总共有多少帧?或者颜色的rgb取值范围是0-1,那么怎样才能表达出超过0-1的顶点坐标呢?
首先关于帧数和顶点数的问题,我们刚才举例子说,1000个顶点和200帧,按道理是1000200的贴图可以表达。不过出于对贴图可压缩的处理习惯,我一般会把贴图的大小设置成2的次幂,比如1000,就会变成1024,而200就会变成256。所以贴图的分辨率会变成1024256。既然贴图大了,那么有些信息就会变得没用,所以看我上面例子所举的贴图,就会发现贴图里面有一部分是纯黑的。这部分就是超出了帧数或者顶点数以外的像素点了。
然后关于坐标表达的问题,有些人可能会用倍数缩放的做法,比如颜色是0.1,然后乘以100倍,就可以变成10米。但这样的做法很难最大限度的利用数据,我的做法是收集整个动画的顶点最大和最小值范围,然后颜色值代表了从最小范围到最大范围的百分比。这样做我感觉就可以最大限度的针对每个动作利用了数值的精度。
当然这样做,就要记录多一些数据了,比如每个动作就需要记录一下顶点的取值范围。
接下来,我用ASE来表达一下播放的过程:
原理很简单,先取到UV2
然后这个UV2的x坐标就是每个顶点的序号,所以不需要处理,处理一下UV2的y坐标,我输入一个frame变量,来控制y坐标的变化:
拿这个变化后的UV2坐标去采样VAT贴图,得到的就是对应每个顶点在frame控制下某一帧的颜色。
之前说过,我会记录顶点的最大最小范围,所以boundMax和boundMin就是这个范围了,把两者相减,然后和颜色值相乘,就可以得到相对最小值的偏移量,加上最小值,就可以得到这个顶点颜色对应的确切的坐标了。
最后,把这个算出来的顶点坐标设置给顶点位置:
顶点坐标要选择绝对坐标:
这样,就可以根据frame的值的变化,读取不同帧的颜色,来改变模型的动作了:
四、 改进
上面的播放方式,存在一个问题。frame实际上的取值范围是0-1,代表的是贴图的uv2的y坐标变化。但实际应用中,我们肯定不需要设置的是一个0-1的范围,而是应该是具体播放到某一帧。比如我一个动作有100帧,如果用代码控制,应该是代码传一个第几帧的参数,动画就要做到绝对的第几帧。
但从上面的数据,我们实际上是做不到这样的效果的,因为贴图里面还有纯黑的在最大帧以外的部分,我们在shader里面也不知道这个贴图实际上y坐标有多少个像素。
于是,把最大帧数和贴图最大高度给记录下来,在shader里面就可以比较准确的通过帧数来控制动画了,这时候会变成这样:
需要注意一下maxHeight就是贴图的最大高度,在计算的时候,取了一个Max
这是防止除数为0的情况。
然后curFrame就是当前需要播放的帧,maxFrame就是这个动画最大有多少帧。
用这个计算出来的AddV替代了最开始时的frame变量。
这时候,就可以很准确的根据输入的第几帧,来播放动画了。
五、 循环播放的控制
上面说的是手动控制帧播放,还需要从外部传入帧的id。那么如果想自动播放怎么办?
很明显,应该是要加入Time的处理了。
把刚才的curFrame改成用Time来输入,就会自己播放了。加入一个speed是控制播放速度。为了还原骨骼动画原来的播放速度,可以在生成VAT的时候,就换算一下speed的值是多少才能还原原来播放的速度。
然后Time后面加入Fract函数,是为了把time的值控制在0-1之间,不要无限的超出贴图UV,不然时间播放长了之后,会出现精度问题。
但这样做,还不够完美,如果很多模型播放同一个循环动画都是按照同一个time和speed来播放,会非常的整齐划一,看起来很怪。所以我加入一个startFrame来控制,当需要播放循环动画的时候,可以随机一个startFrame传进去,动画的开始帧不一样,那么动作就会比较自然,不会太过
六、 混合控制
上面分别介绍了通过具体某一帧来播放,还有通过Time来循环播放的情况。如果是正常来说,分别用2个shader来控制两种需求,是最好的。某些动作需要循环播放,就用循环的shader,某些动作只需要播放一次就停止,就用控制具体某一帧的shader,然后通过C#之类控制当前帧的显示。
不过我们介绍这个技术的前提是想大规模的显示模型,使用SRPBatcher,而SRPBatcher的使用条件之一是需要所有合并的模型都使用同一个Shader变体。所以最好是不要中途换Shader,而是同一个Shader包含了2个功能了。
于是我就把两个代码整理了一下:
分成了手动帧和自动循环两个部分
然后用一个isLoop变量来控制它们两者的融合:
最后变成这样:
这时候,已经比较完美了,可以手动播放帧,又可以循环播放,循环的时候,还可以随机开始帧让动画错开。
七、 关于模型多部分组合和换装
记得之前介绍GPU Skinning插件的时候,有朋友留言说这个插件好像对一个模型有多个网格部件的情况下会出问题。
我当时的回答是,都用这么极端的渲染方式,那么网格模型最好还是合并一下。后来我想了一下,发现这种情况还是有可能出现的,毕竟模型有组合或者换装的需求。
于是我这个版本的阿赵VAT,对这个东西也做了支持。
比如这个小兵,它身上实际上是由多个部位构成:
我可以随意的替换或者隐藏某些部位:
这个实现的手段也很简单,把逐个部件分别的生成VAT,然后采样动作的时候,动作的间隔采样时间控制得准确一点,那么多个部位的动作就可以完全对得上了。
八、 源码
最后给一下ASE生成的源码,有兴趣的朋友可以对着节点看看:
// Made with Amplify Shader Editor
// Available at the Unity Asset Store - http://u3d.as/y3X
Shader "VertexAnimTest5"
{
Properties
{
[HideInInspector] _AlphaCutoff("Alpha Cutoff ", Range(0, 1)) = 0.5
[HideInInspector] _EmissionColor("Emission Color", Color) = (1,1,1,1)
[ASEBegin]_MainTex("MainTex", 2D) = "white" {}
_frameTex("frameTex", 2D) = "white" {}
_boundMax("boundMax", Vector) = (0,0,0,0)
_boundMin("boundMin", Vector) = (0,0,0,0)
_speed("speed", Float) = 1
_maxFrame("maxFrame", Float) = 0
_maxHeight("maxHeight", Float) = 0
_isLoop("isLoop", Float) = 0
_startFrame("startFrame", Float) = 0
_curFrame("curFrame", Float) = 0
[ASEEnd]_speedUp("speedUp", Float) = 1
[HideInInspector] _texcoord( "", 2D ) = "white" {}
//_TessPhongStrength( "Tess Phong Strength", Range( 0, 1 ) ) = 0.5
//_TessValue( "Tess Max Tessellation", Range( 1, 32 ) ) = 16
//_TessMin( "Tess Min Distance", Float ) = 10
//_TessMax( "Tess Max Distance", Float ) = 25
//_TessEdgeLength ( "Tess Edge length", Range( 2, 50 ) ) = 16
//_TessMaxDisp( "Tess Max Displacement", Float ) = 25
}
SubShader
{
LOD 0
Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" "Queue"="Geometry" }
Cull Back
AlphaToMask Off
HLSLINCLUDE
#pragma target 2.0
float4 FixedTess( float tessValue )
{
return tessValue;
}
float CalcDistanceTessFactor (float4 vertex, float minDist, float maxDist, float tess, float4x4 o2w, float3 cameraPos )
{
float3 wpos = mul(o2w,vertex).xyz;
float dist = distance (wpos, cameraPos);
float f = clamp(1.0 - (dist - minDist) / (maxDist - minDist), 0.01, 1.0) * tess;
return f;
}
float4 CalcTriEdgeTessFactors (float3 triVertexFactors)
{
float4 tess;
tess.x = 0.5 * (triVertexFactors.y + triVertexFactors.z);
tess.y = 0.5 * (triVertexFactors.x + triVertexFactors.z);
tess.z = 0.5 * (triVertexFactors.x + triVertexFactors.y);
tess.w = (triVertexFactors.x + triVertexFactors.y + triVertexFactors.z) / 3.0f;
return tess;
}
float CalcEdgeTessFactor (float3 wpos0, float3 wpos1, float edgeLen, float3 cameraPos, float4 scParams )
{
float dist = distance (0.5 * (wpos0+wpos1), cameraPos);
float len = distance(wpos0, wpos1);
float f = max(len * scParams.y / (edgeLen * dist), 1.0);
return f;
}
float DistanceFromPlane (float3 pos, float4 plane)
{
float d = dot (float4(pos,1.0f), plane);
return d;
}
bool WorldViewFrustumCull (float3 wpos0, float3 wpos1, float3 wpos2, float cullEps, float4 planes[6] )
{
float4 planeTest;
planeTest.x = (( DistanceFromPlane(wpos0, planes[0]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos1, planes[0]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos2, planes[0]) > -cullEps) ? 1.0f : 0.0f );
planeTest.y = (( DistanceFromPlane(wpos0, planes[1]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos1, planes[1]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos2, planes[1]) > -cullEps) ? 1.0f : 0.0f );
planeTest.z = (( DistanceFromPlane(wpos0, planes[2]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos1, planes[2]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos2, planes[2]) > -cullEps) ? 1.0f : 0.0f );
planeTest.w = (( DistanceFromPlane(wpos0, planes[3]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos1, planes[3]) > -cullEps) ? 1.0f : 0.0f ) +
(( DistanceFromPlane(wpos2, planes[3]) > -cullEps) ? 1.0f : 0.0f );
return !all (planeTest);
}
float4 DistanceBasedTess( float4 v0, float4 v1, float4 v2, float tess, float minDist, float maxDist, float4x4 o2w, float3 cameraPos )
{
float3 f;
f.x = CalcDistanceTessFactor (v0,minDist,maxDist,tess,o2w,cameraPos);
f.y = CalcDistanceTessFactor (v1,minDist,maxDist,tess,o2w,cameraPos);
f.z = CalcDistanceTessFactor (v2,minDist,maxDist,tess,o2w,cameraPos);
return CalcTriEdgeTessFactors (f);
}
float4 EdgeLengthBasedTess( float4 v0, float4 v1, float4 v2, float edgeLength, float4x4 o2w, float3 cameraPos, float4 scParams )
{
float3 pos0 = mul(o2w,v0).xyz;
float3 pos1 = mul(o2w,v1).xyz;
float3 pos2 = mul(o2w,v2).xyz;
float4 tess;
tess.x = CalcEdgeTessFactor (pos1, pos2, edgeLength, cameraPos, scParams);
tess.y = CalcEdgeTessFactor (pos2, pos0, edgeLength, cameraPos, scParams);
tess.z = CalcEdgeTessFactor (pos0, pos1, edgeLength, cameraPos, scParams);
tess.w = (tess.x + tess.y + tess.z) / 3.0f;
return tess;
}
float4 EdgeLengthBasedTessCull( float4 v0, float4 v1, float4 v2, float edgeLength, float maxDisplacement, float4x4 o2w, float3 cameraPos, float4 scParams, float4 planes[6] )
{
float3 pos0 = mul(o2w,v0).xyz;
float3 pos1 = mul(o2w,v1).xyz;
float3 pos2 = mul(o2w,v2).xyz;
float4 tess;
if (WorldViewFrustumCull(pos0, pos1, pos2, maxDisplacement, planes))
{
tess = 0.0f;
}
else
{
tess.x = CalcEdgeTessFactor (pos1, pos2, edgeLength, cameraPos, scParams);
tess.y = CalcEdgeTessFactor (pos2, pos0, edgeLength, cameraPos, scParams);
tess.z = CalcEdgeTessFactor (pos0, pos1, edgeLength, cameraPos, scParams);
tess.w = (tess.x + tess.y + tess.z) / 3.0f;
}
return tess;
}
ENDHLSL
Pass
{
Name "Forward"
Tags { "LightMode"="UniversalForward" }
Blend One Zero, One Zero
ZWrite On
ZTest LEqual
Offset 0 , 0
ColorMask RGBA
HLSLPROGRAM
#define _RECEIVE_SHADOWS_OFF 1
#define ASE_ABSOLUTE_VERTEX_POS 1
#define ASE_SRP_VERSION 999999
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"
#if ASE_SRP_VERSION <= 70108
#define REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
#endif
struct VertexInput
{
float4 vertex : POSITION;
float3 ase_normal : NORMAL;
float4 ase_texcoord1 : TEXCOORD1;
float4 ase_texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VertexOutput
{
float4 clipPos : SV_POSITION;
#if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
float3 worldPos : TEXCOORD0;
#endif
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) && defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
float4 shadowCoord : TEXCOORD1;
#endif
#ifdef ASE_FOG
float fogFactor : TEXCOORD2;
#endif
float4 ase_texcoord3 : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float3 _boundMin;
float3 _boundMax;
float _curFrame;
float _maxFrame;
float _isLoop;
float _speed;
float _speedUp;
float _startFrame;
float _maxHeight;
#ifdef TESSELLATION_ON
float _TessPhongStrength;
float _TessValue;
float _TessMin;
float _TessMax;
float _TessEdgeLength;
float _TessMaxDisp;
#endif
CBUFFER_END
sampler2D _frameTex;
sampler2D _MainTex;
SAMPLER(sampler_MainTex);
VertexOutput VertexFunction ( VertexInput v )
{
VertexOutput o = (VertexOutput)0;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
float2 texCoord8 = v.ase_texcoord1.xy * float2( 1,1 ) + float2( 0,0 );
float maxFrame67 = _maxFrame;
float manualFrame78 = min( _curFrame , ( maxFrame67 - 1.0 ) );
float mulTime64 = _TimeParameters.x * ( _speed * _speedUp );
float temp_output_57_0 = ( ( frac( mulTime64 ) * maxFrame67 ) + _startFrame );
float temp_output_58_0 = ( temp_output_57_0 - maxFrame67 );
float loopCurFrame71 = ( temp_output_58_0 < 0.0 ? temp_output_57_0 : temp_output_58_0 );
float frameIndex83 = ( ( manualFrame78 * ( 1.0 - _isLoop ) ) + ( _isLoop * loopCurFrame71 ) );
float addV86 = ( frameIndex83 * ( 1.0 / max( _maxHeight , 0.0001 ) ) );
float2 appendResult16 = (float2(texCoord8.x , ( texCoord8.y + addV86 )));
float3 appendResult13 = (float3(tex2Dlod( _frameTex, float4( appendResult16, 0, 0.0) ).rgb));
float3 vertexOffset90 = ( _boundMin + ( ( _boundMax - _boundMin ) * appendResult13 ) );
o.ase_texcoord3.xy = v.ase_texcoord.xy;
//setting value to unused interpolator channels and avoid initialization warnings
o.ase_texcoord3.zw = 0;
#ifdef ASE_ABSOLUTE_VERTEX_POS
float3 defaultVertexValue = v.vertex.xyz;
#else
float3 defaultVertexValue = float3(0, 0, 0);
#endif
float3 vertexValue = vertexOffset90;
#ifdef ASE_ABSOLUTE_VERTEX_POS
v.vertex.xyz = vertexValue;
#else
v.vertex.xyz += vertexValue;
#endif
v.ase_normal = v.ase_normal;
float3 positionWS = TransformObjectToWorld( v.vertex.xyz );
float4 positionCS = TransformWorldToHClip( positionWS );
#if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
o.worldPos = positionWS;
#endif
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) && defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
VertexPositionInputs vertexInput = (VertexPositionInputs)0;
vertexInput.positionWS = positionWS;
vertexInput.positionCS = positionCS;
o.shadowCoord = GetShadowCoord( vertexInput );
#endif
#ifdef ASE_FOG
o.fogFactor = ComputeFogFactor( positionCS.z );
#endif
o.clipPos = positionCS;
return o;
}
#if defined(TESSELLATION_ON)
struct VertexControl
{
float4 vertex : INTERNALTESSPOS;
float3 ase_normal : NORMAL;
float4 ase_texcoord1 : TEXCOORD1;
float4 ase_texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct TessellationFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
VertexControl vert ( VertexInput v )
{
VertexControl o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = v.vertex;
o.ase_normal = v.ase_normal;
o.ase_texcoord1 = v.ase_texcoord1;
o.ase_texcoord = v.ase_texcoord;
return o;
}
TessellationFactors TessellationFunction (InputPatch<VertexControl,3> v)
{
TessellationFactors o;
float4 tf = 1;
float tessValue = _TessValue; float tessMin = _TessMin; float tessMax = _TessMax;
float edgeLength = _TessEdgeLength; float tessMaxDisp = _TessMaxDisp;
#if defined(ASE_FIXED_TESSELLATION)
tf = FixedTess( tessValue );
#elif defined(ASE_DISTANCE_TESSELLATION)
tf = DistanceBasedTess(v[0].vertex, v[1].vertex, v[2].vertex, tessValue, tessMin, tessMax, GetObjectToWorldMatrix(), _WorldSpaceCameraPos );
#elif defined(ASE_LENGTH_TESSELLATION)
tf = EdgeLengthBasedTess(v[0].vertex, v[1].vertex, v[2].vertex, edgeLength, GetObjectToWorldMatrix(), _WorldSpaceCameraPos, _ScreenParams );
#elif defined(ASE_LENGTH_CULL_TESSELLATION)
tf = EdgeLengthBasedTessCull(v[0].vertex, v[1].vertex, v[2].vertex, edgeLength, tessMaxDisp, GetObjectToWorldMatrix(), _WorldSpaceCameraPos, _ScreenParams, unity_CameraWorldClipPlanes );
#endif
o.edge[0] = tf.x; o.edge[1] = tf.y; o.edge[2] = tf.z; o.inside = tf.w;
return o;
}
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[patchconstantfunc("TessellationFunction")]
[outputcontrolpoints(3)]
VertexControl HullFunction(InputPatch<VertexControl, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
[domain("tri")]
VertexOutput DomainFunction(TessellationFactors factors, OutputPatch<VertexControl, 3> patch, float3 bary : SV_DomainLocation)
{
VertexInput o = (VertexInput) 0;
o.vertex = patch[0].vertex * bary.x + patch[1].vertex * bary.y + patch[2].vertex * bary.z;
o.ase_normal = patch[0].ase_normal * bary.x + patch[1].ase_normal * bary.y + patch[2].ase_normal * bary.z;
o.ase_texcoord1 = patch[0].ase_texcoord1 * bary.x + patch[1].ase_texcoord1 * bary.y + patch[2].ase_texcoord1 * bary.z;
o.ase_texcoord = patch[0].ase_texcoord * bary.x + patch[1].ase_texcoord * bary.y + patch[2].ase_texcoord * bary.z;
#if defined(ASE_PHONG_TESSELLATION)
float3 pp[3];
for (int i = 0; i < 3; ++i)
pp[i] = o.vertex.xyz - patch[i].ase_normal * (dot(o.vertex.xyz, patch[i].ase_normal) - dot(patch[i].vertex.xyz, patch[i].ase_normal));
float phongStrength = _TessPhongStrength;
o.vertex.xyz = phongStrength * (pp[0]*bary.x + pp[1]*bary.y + pp[2]*bary.z) + (1.0f-phongStrength) * o.vertex.xyz;
#endif
UNITY_TRANSFER_INSTANCE_ID(patch[0], o);
return VertexFunction(o);
}
#else
VertexOutput vert ( VertexInput v )
{
return VertexFunction( v );
}
#endif
half4 frag ( VertexOutput IN ) : SV_Target
{
UNITY_SETUP_INSTANCE_ID( IN );
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX( IN );
#if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
float3 WorldPosition = IN.worldPos;
#endif
float4 ShadowCoords = float4( 0, 0, 0, 0 );
#if defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
ShadowCoords = IN.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
ShadowCoords = TransformWorldToShadowCoord( WorldPosition );
#endif
#endif
float2 uv_MainTex = IN.ase_texcoord3.xy * _MainTex_ST.xy + _MainTex_ST.zw;
float4 tex2DNode6 = tex2D( _MainTex, uv_MainTex );
float3 BakedAlbedo = 0;
float3 BakedEmission = 0;
float3 Color = tex2DNode6.rgb;
float Alpha = tex2DNode6.a;
float AlphaClipThreshold = 0.5;
float AlphaClipThresholdShadow = 0.5;
#ifdef _ALPHATEST_ON
clip( Alpha - AlphaClipThreshold );
#endif
#ifdef LOD_FADE_CROSSFADE
LODDitheringTransition( IN.clipPos.xyz, unity_LODFade.x );
#endif
#ifdef ASE_FOG
Color = MixFog( Color, IN.fogFactor );
#endif
return half4( Color, Alpha );
}
ENDHLSL
}
九、改进的空间
我这个实现,其实是有限制的,一个很简单的问题,假如模型的顶点很多,比如有一万多面,难道我们就要用一万多像素的贴图吗?
明显是不需要的,面数很多的情况下,可以对上面的算法进行修改,做一个多行的读取。这一步我其实早就已经实现了,曾经导出过2万多面的模型也没问题。不过这个改进版的代码我就不打算放出来了,各位可以自己思考一下。