Custom PBL Splat Shader

I’m trying to create a PBL shader that uses vertex colors to control texture splatting. I’ve got the blending working, but my shader is letting shadows shine through solid geometry somehow:

This is the shader code (also uploaded to PasteBin in case of copy/paste trouble)

Shader "Custom/PBS Splat (Premultiplied)" {
 
  Properties {
 
    //_Color("Color", Color) = (1,1,1)   
 
    _MainTex("Layer 1 Diffuse", 2D) = "white" {}
    [NoScaleOffset] _MainBump("Layer 1 Normals", 2D) = "bump" {}
    [NoScaleOffset] _MainSpecular("Layer 1 Specular", 2D) = "white" {}
    [NoScaleOffset] _MainOcclusion("Layer 1 Occlusion", 2D) = "white" {}
 
    _SecondTex("Layer 2 Diffuse", 2D) = "white" {}
    [NoScaleOffset] _SecondBump("Layer 2 Normals", 2D) = "bump" {}
    [NoScaleOffset] _SecondSpecular("Layer 2 Specular", 2D) = "white" {}
    [NoScaleOffset] _SecondOcclusion("Layer 2 Occlusion", 2D) = "white" {}
 
    _ThirdTex("Layer 3 Diffuse", 2D) = "white" {}
    [NoScaleOffset] _ThirdBump("Layer 3 Normals", 2D) = "bump" {}
    [NoScaleOffset] _ThirdSpecular("Layer 3 Specular", 2D) = "white" {}
    [NoScaleOffset] _ThirdOcclusion("Layer 3 Occlusion", 2D) = "white" {}
 
    //_BumpScale("Scale", Float) = 1.0
    //[NoScaleOffset] _EmissionMap("Emission", 2D) = "black" {}
 
  }
 
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
   
    CGPROGRAM
 
    #pragma surface surf Standard fullforwardshadows
 
    #pragma target 3.0
     
    #define _GLOSSYENV 1
    #include "UnityPBSLighting.cginc"
 
    sampler2D _MainTex;
    sampler2D _MainBump;
    sampler2D _MainSpecular;
    sampler2D _MainOcclusion;
 
    sampler2D _SecondTex;
    sampler2D _SecondBump;
    sampler2D _SecondSpecular;
    sampler2D _SecondOcclusion;
 
    sampler2D _ThirdTex;
    sampler2D _ThirdBump;
    sampler2D _ThirdSpecular;
    sampler2D _ThirdOcclusion;
 
    //sampler2D _EmissionMap;
    //fixed4 _Color;
    //half _BumpScale;
 
    struct Input {
      float2 uv_MainTex : TEXCOORD;
      float4 color : COLOR;
    };
 
    void surf (Input IN, inout SurfaceOutputStandard o) {
      fixed4 l1 = tex2D (_MainTex, IN.uv_MainTex);
      fixed4 l2 = tex2D (_SecondTex, IN.uv_MainTex);
      fixed4 l3 = tex2D (_ThirdTex, IN.uv_MainTex);
 
      o.Albedo = ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)).rgb;
 
      l1 = tex2D(_MainBump, IN.uv_MainTex);
      l2 = tex2D(_SecondBump, IN.uv_MainTex);
      l3 = tex2D(_ThirdBump, IN.uv_MainTex);
      //o.Normal = normalize(UnpackScaleNormal(tex2D(_BumpMap, IN.uv_MainTex), _BumpScale));
 
      o.Normal = ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)).rgb;
 
      l1 = tex2D(_MainSpecular, IN.uv_MainTex);
      l2 = tex2D(_SecondSpecular, IN.uv_MainTex);
      l3 = tex2D(_ThirdSpecular, IN.uv_MainTex);
     
      fixed4 specular = (l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b);
      o.Specular = specular.rgb;
      o.Smoothness = specular.a;
 
      l1 = tex2D(_MainOcclusion, IN.uv_MainTex);
      l2 = tex2D(_SecondOcclusion, IN.uv_MainTex);
      l3 = tex2D(_ThirdOcclusion, IN.uv_MainTex);
 
      o.Occlusion = ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)).g;
    }
 
    ENDCG
  }
 
}

I’m targeting WebGL, so I assume it’s forward and not deferred rendering, thus I’ve enabled all forward shadow types in the shader.

Does anyone have an idea why the shadow is showing through the mesh?

2 Likes

Any luck with this?

Not sure if it’s related, but try adding Fallback “VertexLit” just before the last }

(you should also unpack the normals)

would love the shader when its done and working :slight_smile:

I managed to solve the shadow issue, but I still have a major problem with the specular highlights - they’re way too strong, but only affect specific faces.

@AcidArrow : True. I’m still looking for documentation on Unity’s normal packing. Ideally I would want to first combine the normals and then unpack, but depending on the packing method I may have to unpack thrice and then combine.

1 Like

Nice, did you manage to solve the issue?

Unity uses DXT5nm packing where available, take a look at the UnityCG.cginc file in the builtin shaders to see how they are unpacked, basically x and y are stored in the green and alpha channels, and the z value is computed:

inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
    fixed3 normal;
    normal.xy = packednormal.wy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
#else
    return UnpackNormalDXT5nm(packednormal);
#endif
}

The reason for this is that DXT5 has more precision in the green and alpha channels. You can however pack your normals however you want, and then not mark the texture as a normalmap in the import settings, but you will loose some precision in the normals.

Thanks @steego !

Already found that (maybe I should update this thread more often :smile:) and also compared the results between unpacking thrice and then combining versus combining and unpacking once. No visual difference, so I think the optimized path is okay.

However, I noticed that I can’t replicate the look Unity’s default PBL shader, even if I skip all of my math and just assign texture layer 1 directly. So I’ve put this on ice until Unity 5.0 gets released or there’s some official documentation on how to write custom PBL shaders.

If anyone is interested in the current code, here it is:

Shader "Custom/PBL Splat (Three Textures)" {
  Properties {
    _MainTex ("Albedo (Layer 1)", 2D) = "white" {}
    [NoScaleOffset] _MainBump("Normals (Layer 1)", 2D) = "bump" {}
    [NoScaleOffset] _MainSpecular("Specular (Layer 1)", 2D) = "black" {}
    [NoScaleOffset] _MainOcclusion("Occlusion (Layer 1)", 2D) = "white" {}

    _SecondTex ("Albedo (Layer 2)", 2D) = "white" {}
    [NoScaleOffset] _SecondBump("Normals (Layer 2)", 2D) = "bump" {}
    [NoScaleOffset] _SecondSpecular("Specular (Layer 2)", 2D) = "black" {}
    [NoScaleOffset] _SecondOcclusion("Occlusion (Layer 2)", 2D) = "white" {}

    _ThirdTex ("Albedo (Layer 3)", 2D) = "white" {}
    [NoScaleOffset] _ThirdBump("Normals (Layer 3)", 2D) = "bump" {}
    [NoScaleOffset] _ThirdSpecular("Specular (Layer 3)", 2D) = "black" {}
    [NoScaleOffset] _ThirdOcclusion("Occlusion (Layer 3)", 2D) = "white" {}
  }

  CGINCLUDE
  //#define _GLOSSYENV 1
  #define UNITY_SETUP_BRDF_INPUT SpecularSetup
  ENDCG

  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
   
    CGPROGRAM
    #pragma surface surf Standard

    #include "UnityPBSLighting.cginc"

    sampler2D _MainTex;
    sampler2D _MainBump;
    sampler2D _MainSpecular;
    sampler2D _MainOcclusion;

    sampler2D _SecondTex;
    sampler2D _SecondBump;
    sampler2D _SecondSpecular;
    sampler2D _SecondOcclusion;

    sampler2D _ThirdTex;
    sampler2D _ThirdBump;
    sampler2D _ThirdSpecular;
    sampler2D _ThirdOcclusion;

    struct Input {
      float2 uv_MainTex : TEXCOORD;
      float4 color : COLOR;
    };

    void surf (Input IN, inout SurfaceOutputStandard o) {
      half4 l1 = tex2D (_MainTex, IN.uv_MainTex);
      half4 l2 = tex2D (_SecondTex, IN.uv_MainTex);
      half4 l3 = tex2D (_ThirdTex, IN.uv_MainTex);

      o.Albedo = (
        ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)).rgb
      );
      //o.Albedo = l3.rgb;

      l1 = tex2D (_MainBump, IN.uv_MainTex);
      l2 = tex2D (_SecondBump, IN.uv_MainTex);
      l3 = tex2D (_ThirdBump, IN.uv_MainTex);

      o.Normal = UnpackScaleNormal(
        ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)), 2.5
      );
      //o.Normal = UnpackScaleNormal(l3, 2.5);

      l1 = tex2D (_MainOcclusion, IN.uv_MainTex);
      l2 = tex2D (_SecondOcclusion, IN.uv_MainTex);
      l3 = tex2D (_ThirdOcclusion, IN.uv_MainTex);

      o.Occlusion = (
        ((l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b))
      );
      //o.Occlusion = l3;

      l1 = tex2D (_MainSpecular, IN.uv_MainTex);
      l2 = tex2D (_SecondSpecular, IN.uv_MainTex);
      l3 = tex2D (_ThirdSpecular, IN.uv_MainTex);

      half4 specular = (
        (l1 * IN.color.r) + (l2 * IN.color.g) + (l3 * IN.color.b)
      );
      o.Specular = specular.rgb / 1.5; // Why are these magic numbers required?
      o.Smoothness = specular.a / 6;   // Integer division issue? Nope, values are float!

      //o.Specular = l3.rgb;
      //o.Smoothness = l3.a;
    }
    ENDCG
  }

  // If we can't splat, use Diffuse. That will make everything look like layer 1,
  // but that's probably preferable to VertexLit (flat or gouraud-shaded vertices)
  // with colors that make no sense to the player...
  FallBack "Diffuse"
}

(pastebin)

Also, for Blender users, some things I figured out:

  • Blender’s “decimate” modifier lets you select a vertex group that controls how likely vertices are to be touched by the modifier. This way you can create a highly subdivided mesh, liberally paint splat weights onto it, decimate it to just a bunch of polygons while still preventing “decimate” from destroying vertices by which the mesh connects to the surrounding meshes or vertices at places where you want to have hard changes in the splat weights.

  • I have written a small Blender add-on script that lets you normalize vertex colors. My knowledge about Blender scripting and Python is pretty much zilch, so for some reason the script only works from when you open Blender until you first try to weight paint.