Foliage similar to "The Witness"

I’m gonna assume that this beyond what a single shader can do, but I figured I’d post here anyways.

I’ve been studying the foliage in The Witness and trying to figure out how to recreate the effect in Unity (if at all).
Here’s a screenshot of The Witness if you haven’t seen it before.

If you haven’t played it, the game appears to use a mix of baked and realtime lighting. The direct sunlight casts a shadow, but things like GI appear to be baked.

The foliage has a very “soft” feel to it. Obviously the solid colors and low contrast in the leaf textures are a big part of it, but there’s a lot more to it.

Ignoring the leaf textures, the tree leaves have the following qualities:

  • Leaf normals are not flat (search: normal thief)
  • Are affected by both realtime direct sunlight and baked GI
  • Cast realtime shadows
  • Contribute to global illumination (see wall near pink tree)
  • Use soft-edge cutout it seems (not possible in Deferred IIRC)
  • Leaf polygons fade out the more parallel the camera is with them
  • Subtle vertex animations to simulate light wind blowing

Here’s what I’ve got, using the Standard (Specular) shader and a simple bush model with corrected normals:

Not bad for my first foliage asset, if I do say so myself.

It works like a charm, until you want to make it static:

It’s using custom lightmap properties to allow GI to pass through, but that doesn’t seem to be helping too much on the bush itself.

I’m not really sure what to do about the dark shadows on the bush itself, but I have some ideas:

  • When light hits a polygon, instead of blocking all light, allow a bit to pass through
  • Increase the bounced light intensity on the bush objects only

Pretty sure the above two ideas are impossible.

I would love to hear your guys’ thoughts on how to approach this.

You probably don’t want actual shadows on it. And fading out the parallel facing triangles will also help a lot.

To improve things, it might be enough to just change the normals as if the object was actually a sphere. And removing shadows and adding fading.

For standard shadows, you can’t let a bit pass through. You could use fake self-shadowing that you blend based on the distance the light has to travel through the tree.

The Witness uses alpha to coverage (AlphaToMask in Unity) for it’s trees, grass, and bushes. To use it requires MSAA to be enabled, which means you can’t be using Unity’s deferred or HDR rendering. Alpha to coverage allows for properly depth sorted transparency by abusing MSAA coverage samples, effectively doing alpha test dithering on a sub pixel level. In more simple terms with 4x MSAA you get 4 levels of depth sorted transparency.

For Unity you’ll probably just want to stick to cutout like you’re doing.

Here’s some of the posts The Witness devs did on trees and how they were rendered.
http://the-witness.net/news/2011/06/witness-trees/
http://the-witness.net/news/2013/06/2157/
http://www.artofluis.com/3d-work/the-art-of-the-witness/clouds/

1 Like

Would you mind elaborating on this? Also another Idea I got for the self-shadowing is to use light wrapping in combination with light probes (and just keep the bush not static). Then I could use an invisible static tree for the baked shadow casting.

EDIT: Slight update to code

Here’s the progress I’ve made on the polygon fade:

It’s almost where I want, with the only problem being that the alpha of each polygon doesn’t stack onto the alpha of the polygons behind it. Below is the shader code:

Shader "Foliage/Leaves" {
        Properties {
            _MainTex ("Texture", 2D) = "white" {}
            _CutOff("Cut off", float) = 0.1
        }
        SubShader {
        Tags {"Queue"="AlphaTest" "RenderType"="TransparentCutout" }
        CGPROGRAM
        #pragma surface surf Standard addshadow decal:blend
       #pragma target 3.0

        struct Input {
            float2 uv_MainTex;
            float3 viewDir;
        };

        float _CutOff;
    
        sampler2D _MainTex;
            void surf (Input IN, inout SurfaceOutputStandard o) {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            clip(c.a - _CutOff);
            o.Albedo = c.rgb;
            half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
            o.Alpha = rim;
        }
        ENDCG
        }
        Fallback "Diffuse"
    }

Cutoff and blend are essentially mutually exclusive. You can “fake it” with a two pass shader where you render once with alpha test and again with blend enabled. Here’s a shader I original wrote for another thread that shows this off.

Shader "Custom/Standard Two Sided Soft Blend" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        [NoScaleOffset] _MainTex ("Albedo (RGB)", 2D) = "white" {}
        [Toggle] _UseMetallicMap ("Use Metallic Map", Float) = 0.0
        [NoScaleOffset] _MetallicGlossMap("Metallic", 2D) = "black" {}
        [Gamma] _Metallic ("Metallic", Range(0,1)) = 0.0
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _BumpScale("Scale", Float) = 1.0
        [NoScaleOffset] _BumpMap("Normal Map", 2D) = "bump" {}
        _Cutoff("Alpha Cutoff", Range(0.01,1)) = 0.5
    }
    SubShader {
        Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
        Blend SrcAlpha OneMinusSrcAlpha
        LOD 200
        ZWrite Off
        Cull Off
        Pass {
            ColorMask 0
            ZWrite On
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
       
            #include "UnityCG.cginc"
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };
            sampler2D _MainTex;
            fixed _Cutoff;
            v2f vert (appdata_img v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = v.texcoord;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.texcoord);
                clip(col.a - _Cutoff);
                return 0;
            }
            ENDCG
        }
        Pass
        {
            Tags {"LightMode"="ShadowCaster"}
            ZWrite On
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"
            struct v2f {
                V2F_SHADOW_CASTER;
                float2 texcoord : TEXCOORD1;
            };
            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                o.texcoord = v.texcoord;
                return o;
            }
       
            sampler2D _MainTex;
            fixed _Cutoff;
            float4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.texcoord);
                clip(col.a - _Cutoff);
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
   
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows alpha:fade nolightmap
        #pragma shader_feature _USEMETALLICMAP_ON
        #pragma target 3.0
        sampler2D _MainTex;
        sampler2D _MetallicGlossMap;
        sampler2D _BumpMap;
        struct Input {
            float2 uv_MainTex;
            fixed facing : VFACE;
        };
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        half _BumpScale;
        fixed _Cutoff;
        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            #ifdef _USEMETALLICMAP_ON
            fixed4 mg = tex2D(_MetallicGlossMap, IN.uv_MainTex);
            o.Metallic = mg.r;
            o.Smoothness = mg.a;
            #else
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            #endif
            // Rescales the alpha on the blended pass
            o.Alpha = saturate(c.a / _Cutoff);
            o.Normal = UnpackScaleNormal(tex2D(_BumpMap, IN.uv_MainTex), _BumpScale);
            o.Normal.z *= IN.facing;
        }
        ENDCG
    }
    FallBack "Diffuse"
}
1 Like