概要
メッシュを変形するとUVも一緒に曲がりますが,UVは三角形で補間されるので,ポリゴンフローで見るとガビガビになってしまいます.
これをシェーダーで補正します.ただし,本手法は制約が厳しいので応用先は限定的です.
前提条件
本手法でUVを補正するためには,メッシュが以下の条件を満たしている必要があります.
- メッシュがすべて四角形で構成されている
- UVがすべて格子状に配置されている
- Blenderでカーブからメッシュを生成したときにできるUVなど
- 格子線はU軸・V軸に平行でさえあれば,その間隔は一定でなくてもよい
- 四角形を構成する4頂点の座標とUV座標をジオメトリシェーダー内で取得できる
- 四角形を三角形化する向きがメッシュ全体で同じ
上記の通り制約が厳しく,メッシュそのものをジオメトリシェーダーで生成するようなシェーダーでないと適用が難しいかと思います.
補正方法
UV座標を,その座標における四角形と三角形の辺の長さの比率を新たにUVとして定義し,元のUVを新たなUVで補間することによって補正します.
下の図でいうと,( 赤 ÷ 青 ) を四角形内のローカルV座標として定義し,四角形の下辺・上辺のグローバルV座標 (本来のUV) をローカルV座標で補間します.U座標も同様です.
図を見てわかる通り,補正したいUV座標がどちらの三角形に含まれるかで場合分けが必要になります.
この方法は正確な補正ではないですが,見た目の改善には十分です.
実装例
上の図と同じ向きに三角形を作る前提で実装します.
Shader "Test/UvSmoothing"
{
Properties
{
[Toggle] _EnableSmoothing("Enable Smoothing", Float) = 1
}
SubShader
{
Pass
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
CGPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#pragma multi_compile_instancing
#pragma target 4.6
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2g
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
struct g2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 uvRange : TEXCOORD1;
float2 uvData : TEXCOORD2;
float4 lenData : TEXCOORD3;
UNITY_VERTEX_OUTPUT_STEREO
};
float _EnableSmoothing;
v2g vert (appdata v)
{
v2g o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = v.vertex;
return o;
}
[maxvertexcount(6)]
void geom (triangle v2g IN[3], uint primitive_index : SV_PrimitiveID, inout TriangleStream<g2f> stream)
{
if (primitive_index != 0)
{
return;
}
UNITY_SETUP_INSTANCE_ID(IN[0]);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN[0]);
// 四角形の4つの頂点(適当に配置)
float4 p00 = float4(0, 0, 0, 1);
float4 p01 = float4(0, 1, 0, 1);
float4 p10 = float4(1, 0.1, 0, 1);
float4 p11 = float4(1.1, 0.7, 0, 1);
// 元のUV
float2 uv00 = float2(0, 0);
float2 uv01 = float2(0, 1);
float2 uv10 = float2(1, 0);
float2 uv11 = float2(1, 1);
float4 lenData = float4(
length(p10.xyz - p00.xyz), // 下辺
length(p11.xyz - p01.xyz), // 上辺
length(p01.xyz - p00.xyz), // 左辺
length(p11.xyz - p10.xyz) // 右辺
);
g2f o00, o01, o10, o11;
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o00);
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o01);
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o10);
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o11);
o00.vertex = UnityObjectToClipPos(p00);
o01.vertex = UnityObjectToClipPos(p01);
o10.vertex = UnityObjectToClipPos(p10);
o11.vertex = UnityObjectToClipPos(p11);
o00.uv = uv00;
o01.uv = uv01;
o10.uv = uv10;
o11.uv = uv11;
// 左下のUV座標と右上のUV座標
o00.uvRange = float4(uv00, uv11);
o01.uvRange = float4(uv00, uv11);
o10.uvRange = float4(uv00, uv11);
o11.uvRange = float4(uv00, uv11);
// 四角形1つ分を[0,1]に正規化したUV座標
o00.uvData = float2(0, 0);
o01.uvData = float2(0, 1);
o10.uvData = float2(1, 0);
o11.uvData = float2(1, 1);
o00.lenData = lenData;
o01.lenData = lenData;
o10.lenData = lenData;
o11.lenData = lenData;
// 右下と左上に分割するように三角形を作る
stream.Append(o00);
stream.Append(o01);
stream.Append(o11);
stream.RestartStrip();
stream.Append(o00);
stream.Append(o11);
stream.Append(o10);
stream.RestartStrip();
}
float4 frag (g2f i) : SV_Target
{
float2 uv;
if (_EnableSmoothing < 0.5)
{
// 普通のUV
uv = i.uv;
}
else
{
// UVの補正
uv = i.uvData; // quad内のローカルuv
float2 la = i.lenData.xz; // 下辺と左辺の長さ
float2 lb = i.lenData.yw; // 上辺と右辺の長さ
float2 l = lerp(la, lb, uv.yx);
if (uv.x > uv.y)
{
// 右下の三角形
uv = float2(1 - la.x / l.x * (1 - uv.x), lb.y / l.y * uv.y);
}
else
{
// 左上の三角形
uv = float2(lb.x / l.x * uv.x, 1 - la.y / l.y * (1 - uv.y));
}
uv = lerp(i.uvRange.xy, i.uvRange.zw, saturate(uv));
}
float4 col = float4(0, 0, 0, 1);
col.xy = frac(uv * 10);
return col;
}
ENDCG
}
}
}
適当なメッシュに適用すれば,冒頭の画像の通りの結果が得られます.
雑記
「四角形の4頂点を取得する」というのがなかなかに難しいので,Unity の通常の SkinnedMesh に適用するのは難しいと思います.triangleadj が使えれば何とかなりそうなんですが,Unity では使えないらしいです.
筆者の用途では,GPUパーティクル的にデータ用テクスチャから点の位置を取得してシェーダー内でメッシュを組み立てるため,この手法がギリギリ使えます.
描画負荷を減らすCGの歴史に逆行している気もしますが... まあいいでしょう.