前言
这段时间在玩一款叫第七史诗的游戏,里面有一种怪物身上部分材质不是通过正常的模型UV值进行采样的,怪物魔性的动作和不正常的纹理采样让这个怪物看起来非常的喜感有趣。
刚好最近有读到关于溶解的代码,里面也有实现这种效果的功能,本来想自己实现一下,但是还是遇到了挺多问题的。这篇文章主要也是说一下自己遇到的一些问题,也好以后回顾学习。
先贴上溶解教程的源连接
1.让我们先实现一个可以贴图片的shader把!
首先,我们先新建一个船新的shader文件,删除那些多余的东西,只保存一个图片变量,并把图片贴在模型的uv上,代码如下:
Shader "Custom/StandardTexture" {
Properties {
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Pass {
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
float4 _MainTex_ST;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uvMainTex : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb;
return fixed4(albedo, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
上面的代码功能很简单,就是把图片贴在模型上。我们可以得到一个这样的cube
2.使用世界坐标代替模型UV对图片进行采样
因为采样过程是在片元着色器进行的,我们要把顶点的世界坐标传给片元着色器。首先,在顶点输出结构体里添加用来存储世界坐标的字段。
struct v2f { float4 pos : SV_POSITION; float2 uvMainTex : TEXCOORD0; float3 worldPos : TEXCOORD1; };
然后还要在顶点着色器中把计算好的世界坐标传给片元着色器。
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; }
最后在片元着色器中使用世界坐标代替之前的模型uv值。
fixed4 frag(v2f i) : SV_Target { fixed3 albedo = tex2D(_MainTex, i.worldPos).rgb; return fixed4(albedo, 1); }
我们可以看到,只有前后两面是正确对图片采样的(其实并不能看到后面,但是后面确实是正确的),cube的顶面和左侧的面拿到的是错误的信息,为什么会这样呢?
这是因为我们在使用tex2D函数的时候,第二个参数其实需要的是一个二维的参数,但是我们传入了一个三维的世界坐标。我们没有指明使用世界坐标的哪些分量对图片进行采样,unity默认就会使用世界坐标的xy坐标进行采样。
cube的顶面y值都是相同的,所以要使用世界坐标的xz分量对图片采样我们才能拿到正确的效果。相对的,左右两面需要使用世界坐标的yz分量进行采样。
3.让cube的各个面使用不同的世界坐标进行采样
关于让不同面使用不同坐标进行采样,还是通过看了网上的做法才有了思路,地址在这里。
文章中使用了顶点的法线信息与固定轴向点积,得到法线在轴上的投影来决定采样的坐标。所以我们也通过把法线分解(点积)为三个坐标轴方向的分量,再通过三个分量的大小对世界坐标进行分解与组合。我们在顶点着色器中进行计算,这样可以减少计算量。
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; float NX = dot(v.normal, float3(1.0, 0.0, 0.0)); float NY = dot(v.normal, float3(0.0, 1.0, 0.0)); float NZ = dot(v.normal, float3(0.0, 0.0, 1.0)); o.uvMainTex = o.worldPos.yz * NX + o.worldPos.zx * NY + o.worldPos.xy * NZ; return o; }
还要把片元着色器中原来对世界坐标采样的代码修改为使用我们组合过的新UV值进行采样。
fixed4 frag(v2f i) : SV_Target { fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb; return fixed4(albedo, 1); }
这个cube看起来以切正常,各个面在移动的时候都会根据世界坐标不同对纹理进行采样。
但是,当这个cube旋转起来的时候,有的面就会出现采样不正常的情况。
4.处理旋转问题
为了查找上面的问题,我们把计算后的法线直接作为颜色输出到cube上,检查一下我们计算的过程是否有问题。首先,我们把上面NX,NY,NZ当做颜色的RGB值进行输出:
我们发现,物体的法线并没有根据物体的旋转而旋转,我个人觉得这应该是因为旋转了模型,对应的旋转了模型的顶点,而模型顶点的法线信息是存储在顶点内的,所以旋转顶点并不会对模型法线造成任何影响。所以我尝试着把模型法线转到了世界坐标下:
float3 worldNormal = mul(unity_ObjectToWorld, v.normal); float NX = abs(dot(worldNormal, float3(1.0, 0.0, 0.0))); float NY = abs(dot(worldNormal, float3(0.0, 1.0, 0.0))); float NZ = abs(dot(worldNormal, float3(0.0, 0.0, 1.0)));
更改后的效果如下:
我们把全新的法线应用到之前的代码中,可以发现我们已经一定程度上解决了之前的问题。注意上面使用了abs函数,是因为点积后的结果是分正负的,如果不取绝对值会导致cube的三个面的值是负的,输出到cube上的结果是黑色的。如果我们使用了取绝对值的分量,在角度达到某个值的时候旋转会反转:
取消绝对值后的旋转:
最后附上完整的shader代码:
Shader "Custom/StandardTexture" {
Properties {
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Pass {
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
float4 _MainTex_ST;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uvMainTex : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = mul(unity_ObjectToWorld, v.normal);
float NX = dot(worldNormal, float3(1.0, 0.0, 0.0));
float NY = dot(worldNormal, float3(0.0, 1.0, 0.0));
float NZ = dot(worldNormal, float3(0.0, 0.0, 1.0));
o.uvMainTex = o.worldPos.yz * NX + o.worldPos.zx * NY + o.worldPos.xy * NZ;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb;
return fixed4(albedo, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
番外:基于屏幕坐标采样
得到了热心网友的反馈说,Epic7中可能不是通过世界坐标采样的,而是通过屏幕坐标进行采样的。好像确实是这样,那我们就在这里补充一下如何对屏幕坐标进行采样。我们可以使用unity提供的获取屏幕坐标的函数ComputeScreenPos()
,然后再对得到的屏幕坐标进行一下齐次运算就可以得到视口空间下的坐标。最后使用视口坐标对图片采样就可以啦。
效果就是这样啦,最后是代码:
v2f vert(a2v v) {
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeScreenPos(o.pos);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float2 wcoord = i.scrPos.xy/i.scrPos.w;
fixed3 albedo = tex2D(_MainTex, wcoord).rgb;
return fixed4(albedo, 1);
}
结语
虽然我们在一定程度上解决了第三个问题,但是在cube旋转到一定程度上还是会有几个面采样结果很奇怪的问题,在单一轴向旋转时都会有一些问题。
由于我也是刚刚开始学习shader,个人水平有限,暂时还没想到好的解决办法,如果有好的解决方案也希望大神不吝赐教。