Hello.
I’m making a shader for my game with some specific features, in particular:
- Triplanar mapping with Y+/Y- support (for floors and ceilings to receive different textures)
- Forced lightmapping (manually selected .exr is decoded and any lightmapping from level properties is ignored - which is necessary for prefabbed pieces used through multiple levels).
- Diffuse/spec/normal shading for the surfaces to be able to react to flashlights and other realtime light sources.
- Cancellation of ambient light contribution (which is already stored in the lightmap and would be multiplied otherwise).
It’s necessary for the following workflow:
- Make modular level geometry, for example, a set of tunnel sections of set length, without any UV1 texture mapping, just pure shapes
- Bring the models to a specially configured lightmapping level, bake the lighting (strictly with one map per object material, no atlasing)
- Steal all resulting .exr files and place them into level-independent texture folder of your project with appropriate import settings
- Manually assign .exr files to the materials of those objects (using shaders that decode them and have an appropriate slot).
- Assign diffuse/normal maps of walls, ceilings and floors to X/Y+/Y-/Z slots of a material, with the triplanar mapping in a shader applying them to the surfaces properly.
- Save objects as prefabs and reuse the modular pieces across any levels you want without the need to store hundreds of retundant lightmaps and wasting hundreds of hours to rebake the same environment pieces all over again.
I’ve found a great example of a triplanar shader by NoiseCrime and modified it to achieve all of the goals above. For optimization sake I should probably shrink the normal mapping to just two texture samplers using RG+BA packing of two normal maps in one file, but otherwise it’s a nice shader. Here is the code:
Shader "Triplanar/Bumped Specular LM"
{
Properties
{
_Color ("Main Color", Color) = (1,1,1,0.5)
_SpecColor ("Specular Color", Color) = (1.0, 1.0, 1.0, 1.0)
_Shininess ("Shininess", Range (0.03, 1.0)) = 0.078125
_TexScale ("Texture Size", Float) = 0.05
_BlendPlateau ("Border Area", Range (0.0, 1.0)) = 0.2
_MainTex ("X (Wall | RGBA / Diffuse+Specular)", 2D) = "white" {}
_MainTex2 ("Y- (Ceiling | RGBA / Diffuse+Specular)", 2D) = "white" {}
_MainTex4 ("Y+ (Floor | RGBA / Diffuse+Specular)", 2D) = "white" {}
_MainTex3 ("Z (Wall | RGBA / Diffuse+Specular)", 2D) = "white" {}
_BumpMap1 ("X (Wall | RGB / Normal)", 2D) = "bump" {}
_BumpMap2 ("Y+ (Ceiling | RGB / Normal)", 2D) = "bump" {}
_BumpMap4 ("Y+ (Floor | RGB / Normal)", 2D) = "bump" {}
_BumpMap3 ("Z (Wall | RGB / Normal)", 2D) = "bump" {}
_LightmapNew ("Lightmap", 2D) = "white" {}
}
Category
{
SubShader
{
ZWrite On
Tags { "RenderType"="Opaque" }
LOD 400
CGPROGRAM
#pragma target 3.0
#pragma multi_compile_builtin
#pragma surface surf BlinnPhong nolightmap vertex:vertLocal
sampler2D _MainTex;
sampler2D _MainTex2;
sampler2D _MainTex3;
sampler2D _MainTex4;
sampler2D _BumpMap1;
sampler2D _BumpMap2;
sampler2D _BumpMap3;
sampler2D _LightmapNew;
float4 _LightmapNew_ST;
fixed4 _Color;
half _Shininess;
half _TexScale;
half _BlendPlateau;
struct Input
{
float3 thisPos;
float3 thisNormal;
float factor;
float2 lmUV;
};
// Vertex program is determined in pragma above
void vertWorld (inout appdata_full v, out Input o)
{
o.thisNormal = mul(_Object2World, float4(v.normal, 0.0f)).xyz;
o.thisPos = mul(_Object2World, v.vertex);
o.lmUV = v.texcoord1.xy * _LightmapNew_ST.xy + _LightmapNew_ST.zw;
}
void vertLocal (inout appdata_full v, out Input o)
{
o.thisNormal = v.normal;
o.thisPos = v.vertex;
o.factor = o.thisNormal.y;
o.lmUV = v.texcoord1.xy * _LightmapNew_ST.xy + _LightmapNew_ST.zw;
}
void surf (Input IN, inout SurfaceOutput o)
{
// Determine the blend weights for the 3 planar projections.
half3 blend_weights = abs( IN.thisNormal.xyz ); // Tighten up the blending zone:
blend_weights = (blend_weights - _BlendPlateau); // (blend_weights - 0.2) * 7; * 7 has no effect.
blend_weights = max(blend_weights, 0); // Force weights to sum to 1.0 (very important!)
blend_weights /= (blend_weights.x + blend_weights.y + blend_weights.z ).xxx;
// Now determine a color value and bump vector for each of the 3
// projections, blend them, and store blended results in these two vectors:
half4 blended_color; // .w hold spec value Not true in this shader
half3 blended_bumpvec;
// Compute the UV coords for each of the 3 planar projections.
// tex_scale (default ~ 1.0) determines how big the textures appear.
half2 coord1 = IN.thisPos.zy * _TexScale;
half2 coord2 = IN.thisPos.zx * _TexScale;
half2 coord3 = IN.thisPos.xy * _TexScale;
// Sample color maps for each projection, at those UV coords.
half4 col1 = tex2D(_MainTex, coord1);
half4 col2 = tex2D(_MainTex2, coord2);
half4 col3 = tex2D(_MainTex3, coord3);
half4 col4 = tex2D(_MainTex4, coord2);
// Sample bump maps too, and generate bump vectors. (Note: this uses an oversimplified tangent basis.)
// Using Unity packed normals (_Y_X), but we don't unpack since we don't need z.
half2 bumpVec1 = tex2D(_BumpMap1, coord1).wy * 2 - 1; // To use standard normal maps change wy to xy
half2 bumpVec2 = tex2D(_BumpMap2, coord2).wy * 2 - 1; // To use standard normal maps change wy to xy
half2 bumpVec3 = tex2D(_BumpMap3, coord3).wy * 2 - 1; // To use standard normal maps change wy to xy
half3 bump1 = half3(0, bumpVec1.x, bumpVec1.y);
half3 bump2 = half3(bumpVec2.y, 0, bumpVec2.x);
half3 bump3 = half3(bumpVec3.x, bumpVec3.y, 0);
float factor = clamp (IN.factor.x, 0, 1);
if (factor < 0.5) factor = 0;
else factor = 1;
// Finally, blend the results of the 3 planar projections
blended_color = col1.xyzw * blend_weights.xxxx + lerp (col2.xyzw * blend_weights.yyyy, col4.xyzw * (blend_weights.yyyy), factor) + col3.xyzw * blend_weights.zzzz;
blended_color.rgb *= DecodeLightmap(tex2D(_LightmapNew, IN.lmUV));
blended_bumpvec = bump1.xyz * blend_weights.xxx + bump2.xyz * blend_weights.yyy + bump3.xyz * blend_weights.zzz;
half4 c = blended_color.rgba * _Color.rgba * 2; // * 2 used so that neutral color would be 128,128,128 and coloration would be possible without darkening
o.Albedo = c.rgb / (0.5 * UNITY_LIGHTMODEL_AMBIENT.rgb);
o.Alpha = c.a;
o.Gloss = blended_color.a;
o.Specular = _Shininess;
o.Normal = normalize( half3(0,0,1) + blended_bumpvec.xyz);
}
ENDCG
}
}
FallBack "Diffuse"
}
And here are the results:
So far, so good. Unfortunately, I got stuck with some weird issues when trying to remove the ambient light contribution. The most popular and widely suggested way of doing it is, of course, adding this:
Unfortunately, for some reason, using that flag turns the output of the shader completely black. No matter what I tried to modify in other parts of the shader, the issue persisted. So I had to try other methods to kill the ambient contribution that would otherwise ruin the look of the manually applied lightmaps, and found one through experimentation:
The result looks to be very close to the brightness of ambient-affected reference objects I’ve used, so it’s probably a correct way of removing the ambient component. Unfortunately, I have a very weird issue arising from that part.
As you can probably guess, for everything to look proper, I need ambient light color of a level where the lightmapping process initially occurred to be identical to the ambient light color of every single level I use extracted lightmaps on. In my case, it’s 40,40,40 dark gray.
The issue is that, for some reason, UNITY_LIGHTMODEL_AMBIENT.rgb often jumps to an order of magnitude brighter value under very weird circumstances. So far I’ve found three triggers:
- Using a scroll wheel on the inspector panel in the Editor temporarily makes it brighter for a fraction of every second wheel is active (the animated GIF above depicts the Scene view as I scroll through the inspector of a material).
- Turning the player camera into a certain range of rotations (specifically a camera in the Game mode, nothing in the Scene view camera orientation triggers it) causes a jump in brightness of UNITY_LIGHTMODEL_AMBIENT.rgb received by the shader that persists until you turn away.
- Using Game mode sometimes turns every single surface using this shader brightly lit with the same UNITY_LIGHTMODEL_AMBIENT.rgb contribution while nothing of out of order is seen in the game view (no correlation with camera rotation here, just happens on arbitrary levels).
Obviously, no shader should react to a scroll wheel or rotation of a player camera in that manner, so something supremely weird is going on here. I have no idea how to fix it, and so far only came up with a following workaround involving an additional manually set value I have to keep an eye on:
_AmbientColor ("Ambient Color", Color) = (1,1,1,0.5)
[...]
o.Albedo = c.rgb / _AmbientColor;
Now, if I set _AmbientColor to 40,40,40, I get consistent ambient intensity if level render settings contain the same color, and I don’t get the same strange jumps in the received brightness. While it’s an acceptable workaround, I’m still curious why would UNITY_LIGHTMODEL_AMBIENT.rgb value jump so much on such weird trigger events. Any ideas?