1

I’m working on a project in Unity where I’m using a compute shader to render grass. The rendering path is Forward+, and I’m trying to make the additional lights affect the grass. I want to achieve shadows cast on the grass by objects between the additional lights and the grass. To do this, I’m writing a custom shader.

enter image description here This image is the result of using Shader Graph to create grass.

enter image description here This image shows the corresponding Shader Graph.

// This describes a vertex on the generated mesh
struct DrawVertex
{
    float3 positionWS; // The position in world space
    float2 uv;
};

// A triangle on the generated mesh
struct DrawTriangle
{
    float3 normalOS;
    float3 diffuseColor;
    float4 extraBuffer;
    DrawVertex vertices[3]; // The three points on the triangle
};

// A buffer containing the generated mesh
StructuredBuffer<DrawTriangle> _DrawTriangles;
float _OrthographicCamSizeTerrain;
float3 _OrthographicCamPosTerrain;

//get the data from the compute shader
void GetComputeData_float(uint vertexID, out float3 worldPos, out float3 normal, out float2 uv, out float3 col,
                          out float4 extraBuffer)
{
    DrawTriangle tri = _DrawTriangles[vertexID / 3];
    DrawVertex input = tri.vertices[vertexID % 3];
    worldPos = input.positionWS;
    normal = tri.normalOS;
    uv = input.uv;
    col = tri.diffuseColor;

    // for some reason doing this with a comparison node results in a glitchy alpha, so we're doing it here, if your grass is at a point higher than 99999 Y position then you should make this even higher or find a different solution
    if (tri.extraBuffer.x == -1)
    {
        extraBuffer = float4(99999, tri.extraBuffer.y, tri.extraBuffer.z, tri.extraBuffer.w);
    }
    else
    {
        extraBuffer = tri.extraBuffer;
    }
}

// Calculate world space UV for blending
void GetWorldUV_float(float3 worldPos, out float2 worldUV)
{
    float2 uv = worldPos.xz - _OrthographicCamPosTerrain.xz;
    uv = uv / (_OrthographicCamSizeTerrain * 2);
    uv += 0.5;
    worldUV = uv;
}

This code is the Grass.hlsl file used in the Shader Graph.


enter image description here This image is the result of the custom shader I created.

Shader "Custom/TestGrass"
{
    Properties
    {
        [Toggle(BLEND)] _BlendFloor("Blend with floor", Float) = 0
        _BlendMult("Blend Multiply", Range(0, 5)) = 1
        _BlendOff("Blend Offset", Range(0, 1)) = 1
        [HDR] _AmbientAdjustmentColor("Ambient Adjustment Color", Color) = (0.5, 0.5, 0.5, 1)
        [HDR] _ShadowColor("Shadow Color", Color) = (0.5, 0.5, 0.5, 1)
        [HideInInspector] _BaseMap("Base Color", 2D) = "white" {}
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            //            "RenderType" = "Opaque"
            //            "UniversalMaterialType" = "Lit"
            //            "Queue" = "AlphaTest"
        }

        Pass
        {
            Name "Universal Forward"
            Tags
            {
                "LightMode" = "UniversalForward"
            }

            Cull Off
            //            Blend One Zero
            //            ZTest LEqual
            //            ZWrite On
            //            AlphaToMask On

            HLSLPROGRAM
            #pragma target 4.5
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature BLEND

            // #pragma multi_compile_instancing

            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ _FORWARD_PLUS
                #pragma multi_compile _ SHADOWS_SHADOWMASK

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
            #include "Grass.hlsl"
            #include "CustomLighting.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float4 _TopTint;
                float4 _BottomTint;
                float _BlendMult, _BlendOff;
                uniform TEXTURE2D(_TerrainDiffuse);
                uniform SAMPLER(sampler_TerrainDiffuse);
                float4 _AmbientAdjustmentColor;
                float4 _ShadowColor;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
                uint vertexID : SV_VertexID;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
                float2 uv : TEXCOORD2;
                float3 diffuseColor : TEXCOORD3;
                float4 extraBuffer : TEXCOORD4;
            };

            float CalculateVerticalFade(Varyings input)
            {
                float blendMul = input.uv.y * _BlendMult;
                float blendAdd = blendMul + _BlendOff;
                return saturate(blendAdd);
            }

            float4 CalculateBaseColor(Varyings input, float verticalFade)
            {
                return lerp(_BottomTint, _TopTint * _AmbientAdjustmentColor, verticalFade) * float4(
                    input.diffuseColor, 1);
            }

            float4 BlendWithTerrain(Varyings input, float verticalFade)
            {
                float2 uv = input.worldPos.xz - _OrthographicCamPosTerrain.xz;
                uv /= _OrthographicCamSizeTerrain * 2;
                uv += 0.5;

                float4 terrainForBlending = SAMPLE_TEXTURE2D(_TerrainDiffuse, sampler_TerrainDiffuse, uv);
                return lerp(terrainForBlending,
                            terrainForBlending + (_TopTint * float4(input.diffuseColor, 1) * _AmbientAdjustmentColor),
                            verticalFade);
            }

            void CalculateCutOff(float extraBufferX, float worldPosY)
            {
                float cutOffTop = extraBufferX >= worldPosY ? 1 : 0;
                clip(cutOffTop - 0.01);
                // if (cutOffTop == 0)
                // {
                //     clip(-1);
                // }
            }

            float3 CalculateLighting(float3 worldPos, float3 normalWS)
            {
                float3 lightingColor;
                AdditionalLights(worldPos, normalWS, lightingColor);
                return lightingColor;
            }

            float GetMainLightShadow(Varyings input)
            {
                float shadow = 0;
                half4 shadowCoord = TransformWorldToShadowCoord(input.worldPos);
                #if _MAIN_LIGHT_SHADOWS_CASCADE || _MAIN_LIGHT_SHADOWS
                    Light mainLight = GetMainLight(shadowCoord);
                    shadow = mainLight.shadowAttenuation;
                #else
                Light mainLight = GetMainLight();
                shadow = mainLight.shadowAttenuation;
                #endif
                return shadow;
            }

            Varyings vert(Attributes input)
            {
                Varyings output;
                GetComputeData_float(input.vertexID, output.worldPos, output.normalWS, output.uv,
                                     output.diffuseColor, output.extraBuffer);
                output.positionHCS = TransformObjectToHClip(output.worldPos);

                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                // Calculate vertical fade factor
                float verticalFade = CalculateVerticalFade(input);

                // Calculate base color
                float4 finalColor = CalculateBaseColor(input, verticalFade);

                // Blend with terrain if the BLEND toggle is enabled
                #if defined(BLEND)
                finalColor = BlendWithTerrain(input, verticalFade);
                #endif

                // Apply cut-off based on the extra buffer value
                CalculateCutOff(input.extraBuffer.x, input.worldPos.y);

                // Shadow calculation
                float mainLightShadow = GetMainLightShadow(input);

                if (mainLightShadow <= 0)
                {
                    finalColor.rgb *= _ShadowColor.rgb;
                }

                // finalColor.rgb += CalculateLighting(input.worldPos, input.normalWS);
                finalColor = AdditionalLightsTest(input.worldPos, input.normalWS, finalColor, _ShadowColor);

                return finalColor;
            }
            ENDHLSL
        }
    }
    Fallback "Universal Render Pipeline/Lit"
}

This code is the custom shader file I created: GrassTestShader.shader.

void AdditionalLights(float3 WorldPos, float3 WorldNormal, out float3 Diffuse)
{
    float3 diffuseColor = 0;
    uint pixelLightCount = GetAdditionalLightsCount();
    InputData inputData;
    float4 screenPos = ComputeScreenPos(TransformWorldToHClip(WorldPos));
    inputData.normalizedScreenSpaceUV = screenPos.xy / screenPos.w;
    inputData.positionWS = WorldPos;

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, WorldPos);
        #ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            // Blinn-Phong
            float3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
            diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal);
        }
    LIGHT_LOOP_END

    Diffuse = diffuseColor;
}

float4 AdditionalLightsTest(float3 worldPos, float3 worldNormal, float4 finalColor, float4 shadowColor)
{
    float3 diffuseColor = 0;
    uint pixelLightCount = GetAdditionalLightsCount();
    InputData inputData;
    float4 screenPos = ComputeScreenPos(TransformWorldToHClip(worldPos));
    inputData.normalizedScreenSpaceUV = screenPos.xy / screenPos.w;
    inputData.positionWS = worldPos;

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, worldPos);
        #ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            float3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
            diffuseColor += LightingLambert(attenuatedLightColor, light.direction, worldNormal);
            if (light.shadowAttenuation <= 0)
            {
                finalColor.rgb *= shadowColor.rgb;
            }
        
        }
    LIGHT_LOOP_END

    finalColor.rgb += diffuseColor;

    return finalColor;
}

This code is the CustomLighting.hlsl file used in GrassTestShader.shader

In the AdditionalLightsTest function, I expected that using ‘finalColor.rgb *= shadowColor.rgb;’ would result in the shadow of the object between the additional light and the grass appearing on the grass, as I am retrieving the shadow from the main light this way. However, contrary to my expectations, nothing changed.

So, the conclusion is that I want to convert the current Shader Graph result into shader code. When comparing the results from the graph and the code, there is an object between the additional light and the grass, but the shadow of this object is only appearing on the grass in the result from the graph. I want to achieve this in the code as well.

I compared the following parts in GrassTestShader.shader:

if (mainLightShadow <= 0)
{
    finalColor.rgb *= _ShadowColor.rgb;
}

with this part in the AdditionalLightsTest function:

if (light.shadowAttenuation <= 0)
{
    finalColor.rgb *= shadowColor.rgb;
}

I tried flipping the conditions and multiplying by 0, and I discovered that it behaves differently compared to the main light. However, I was unable to achieve the desired result with this approach.

When I multiplied by 0 in the main light, the shadow literally turned black. In contrast, when I multiplied by 0 in the additional light, nothing changed. Additionally, when I reversed the condition to:

if (light.shadowAttenuation > 0)
{
    finalColor.rgb *= 0;
}

the result appeared as shown in the picture below, which made me even more confused. enter image description here

How should I approach getting the shadow of the object between the additional light and the grass to appear on the grass?

================================================================

UPDATE 2024.10.07

The issue was that after performing finalColor.rgb *= shadowColor.rgb;

I was adding finalColor.rgb += diffuseColor; below LIGHT_LOOP_END.

As a result, I was able to render the shadows of objects between the additional light and the grass correctly, but there was still one problem.

https://youtu.be/6kCWzBD4d9g

In this video, you can see that shadows appear jagged, like steps, along the boundaries of the light. I need to figure out how to remove this issue.

float4 AdditionalLightsTest(float3 worldPos, float3 worldNormal, float4 finalColor, float4 shadowColor,
                            float additionalLightIntensity)
{
    float3 diffuseColor = 0;
    uint pixelLightCount = GetAdditionalLightsCount();
    InputData inputData;
    float4 screenPos = ComputeScreenPos(TransformWorldToHClip(worldPos));
    inputData.normalizedScreenSpaceUV = screenPos.xy / screenPos.w;
    inputData.positionWS = worldPos;

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light;
        #if _MAIN_LIGHT_SHADOWS_CASCADE || _MAIN_LIGHT_SHADOWS
        half4 shadowMask = CalculateShadowMask(inputData);
        light = GetAdditionalLight(lightIndex, worldPos, shadowMask);
        #else
        light = GetAdditionalLight(lightIndex, worldPos);
        #endif
        #ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            if (light.shadowAttenuation > 0)
            {
                float3 attenuatedLightColor = light.color * light.distanceAttenuation * light.shadowAttenuation *
                    additionalLightIntensity;
                diffuseColor += LightingLambert(attenuatedLightColor, light.direction, worldNormal);
            }
            else
            {
                finalColor.rgb *= shadowColor.rgb;
            }
        }
    LIGHT_LOOP_END

    finalColor.rgb += diffuseColor;

    return finalColor;
}

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.