Tuesday, January 24, 2012

The joys of using world space / procedural UVs for Unity3D

NEW, 9 July 2013: I've detailed a different implementation of the same effect, better for texturing smooth bumpy surface / terrain, in another post -- "A Smoother Triplanar Shader." I still think this way is good for some things too, though.

One of the greatest benefits of old Quake-lineage BSP systems is managed UVs for world geometry; move your polygons wherever you want, and let shaders texture and tile them properly. It lets level designers focus on building, and frees environment modelers to focus on geometry rather than the mundane work of UVing and texturing yet another concrete wall.

If you're an indie developer doing the work of both, well, any shortcuts are welcome. And if you're not thinking about these things, then just ask yourself whether the player's going to look at this thing you're modeling for more than a few seconds. Just use a block and let the engine worry about it.

If you're an actual graphics / shader programmer, you can do pretty nifty stuff with this technique: Tom Betts at Big Robot is working on a not-Minecraft, and talks about using "voxel skinning and virtual texturing" which sure sounds and looks rather pretty. It's kind of similar to what Valve used for the caves in Half-Life 2: Episode Two -- let the computers do math and walk away!

But I'm not Mr. Betts, so I'm using a much more pitiful and simplistic thing.

Here's the shader I've been using for my projects, slightly modified from something I found on Unity3D Answers a long time ago. I found it after a lot of fruitless digging through significantly worse implementations -- one editor script destructively re-UV'd all the meshes in your scenes, the other script did some weird thing with material offsets -- just leave that all at the door and use a shader-based solution, trust me.

Shader "Custom/World UV Test" {

Properties {
_Color ("Main Color", Color) = (1,1,1,1)
_MainTexWall2 ("Wall Side Texture (RGB)", 2D) = "surface" {}
_MainTexWall ("Wall Front Texture (RGB)", 2D) = "surface" {}
_MainTexFlr2 ("Flr Texture", 2D) = "surface" {}
_Scale ("Texture Scale", Float) = 0.1
}

SubShader {


Tags { "RenderType"="Opaque" }

CGPROGRAM
#pragma surface surf Lambert

struct Input {
float3 worldNormal;
float3 worldPos;
};

sampler2D _MainTexWall;
sampler2D _MainTexWall2;
sampler2D _MainTexFlr2;
float4 _Color;
float _Scale;

void surf (Input IN, inout SurfaceOutput o) {
float2 UV;
fixed4 c;

if(abs(IN.worldNormal.x)>0.5) {
UV = IN.worldPos.yz; // side
c = tex2D(_MainTexWall2, UV* _Scale); // use WALLSIDE texture
} else if(abs(IN.worldNormal.z)>0.5) {
UV = IN.worldPos.xy; // front
c = tex2D(_MainTexWall, UV* _Scale); // use WALL texture
} else {
UV = IN.worldPos.xz; // top
c = tex2D(_MainTexFlr2, UV* _Scale); // use FLR texture
}

o.Albedo = c.rgb * _Color;
}

ENDCG
}

Fallback "VertexLit"
}

NOTE: I encountered a lot of bugs on Unity 3.5 Flash, with this shader -- something about too many shader instructions or something -- so for the Ico-ish game, I actually have two separate shaders that have split the UVing code up: one for walls, and one for floors. The performance hit isn't that bad.