WinCNT

높이 안개(Height Fog)를 Global Shader Variable로 변경하기! 본문

Unity/URP or Shader 관련

높이 안개(Height Fog)를 Global Shader Variable로 변경하기!

WinCNT_SSS 2024. 9. 18. 22:36

서론

높이 안개(Height Fog)를 구현한 건 좋은데 구조에 좀 불만이 있었다

이대로라면 여러 귀찮은 점이 발생하므로 Global Shader Variable를 사용해 수정하기로 했다


무엇이 문제인가?

그 때는 셰이더가 하나여서 우선 Height Fog에 관련된 변수들을 전부 CBUFFER에 넣고 구현했다

CBUFFER_START(UnityPerMaterial)
    //...생략...

    // Height Fog
    sampler2D _HeightFogNoise;
    float     _HeightFogNoisePower;
    float     _HeightFogNoiseSpeedX;
    float     _HeightFogNoiseSpeedY;
    half4     _HeightFogColor;
    float     _HeightFogDensity;
    float     _MaxFogHeight;

    //...생략...
CBUFFER_END

 

문제는 이러면 각각의 머테리얼마다 Height Fog 관련 설정을 해야 한다

(아티스트가)매번 관련 머티리얼들을 선택해서 수정해야 한다!

 

또한 사용하는 모든 셰이더에 Height Fog 관련 변수와 함수를 각각 구현해줘야 한다!!!

안 그러면 당연하게도 일부 오브젝트만 Height Fog의 영향을 안 받는 케이스가 보인다

빨간색과 녹색의 오브젝트는 다른 커스텀 셰이더를 사용 중이다


우선은 Height Fog를 라이브러리로 만들자

상술한 문제를 해결하기 위해 Height Fog를 라이브러리로 만들기로 했다

실제 폴더에 텍스트 파일을 하나 만들고 적절한 이름을 설정한 뒤 확장자를 hlsl로 변경했다

 

거기에 Height Fog와 관련된 변수와 함수들을 옮겼다

// Height Fog
sampler2D _HeightFogNoise;
float     _HeightFogNoisePower;
float     _HeightFogNoiseSpeedX;
float     _HeightFogNoiseSpeedY;
half4     _HeightFogColor;
float     _HeightFogDensity;
float     _MaxFogHeight;

float3 MixUniformHeightFog(
    float3 color,
    float3 heightFogcolor,
    float3 objectPos,
    float3 cameraPos,
    float heightFogDensity,
    float maxFogHeight,
    sampler2D noiseTex,
    float2 noiseUV,
    float noiseUVScrollX,
    float noiseUVScrollY,
    float noisePower)
{
    noiseUV.x += frac(_Time.y * noiseUVScrollX);
    noiseUV.y += frac(_Time.y * noiseUVScrollY);
    const float noise = tex2D(noiseTex, noiseUV).r * noisePower;
    
    objectPos = objectPos + noise;
    float3 camToObj = cameraPos - objectPos;

    float t = 0.0;
    const float a = objectPos.y < maxFogHeight ? 0 : cameraPos.y;
    const float b = cameraPos.y > maxFogHeight ? maxFogHeight : -maxFogHeight;
    const float c = cameraPos.y > maxFogHeight ? -objectPos.y : 0;
    
    float d = a + b + c;
    d = max(objectPos.y, cameraPos.y) < maxFogHeight ? camToObj.y : d;
    d = min(objectPos.y, cameraPos.y) > maxFogHeight ? 0 : d;
    t = saturate(d / camToObj.y);
    
    const float distance = length(camToObj) * t;
    const float heightFogFactor = exp2(-heightFogDensity * distance * LOG2_E);
    color = lerp(heightFogcolor.rgb, color, heightFogFactor);
    return color;
}

 

참고로 HeightFogLib.hlsl에는 Core.hlsl가 없기 때문에 몇몇 부분이 컴파일 에러가 난다

 

include 관리를 잘 하면 없앴을 수 있겠지만 매우 귀찮기 때문에 그냥 셰이더 파일에서 순서를 맞춰서 include하기로 했다

 

Height Fog 키워드는 _HEIGHT_FOG_ON에서 _HEIGHT_FOG로 변경했다(그 이유는 후술)

 

Height Fog 키워드의 위치도 Lib로 옮길까 했지만 샘플치고 경직되는 느낌이 있어서 그대로 뒀다

(제대로 된 설계로 정해진 셰이더만 딱 만들다면 넣는 게 나을듯?)


SRP Batcher가 not compatible 되는데요??

이걸로 완성!…이면 좋겠지만 아쉽게도 그렇지 않다

Height Fog 관련 변수들을 HeightFogLib.hlsl에 옮겼기 때문에 SRP Batcher가 not compatible이 되어 버린다!

 

Properties에 있는 Height Fog 관련 변수들이 HeightFogLib.hlsl에 있으니 다른 CBUFFER라고 인식해서 SRP Batcher가 작동하지 않게 되는 것으로 보인다

그렇다고 HeightFogLib.hlsl에 있는 Height Fog와 관련 변수들을 CBUFFER_START(UnityPerMaterial)로 감싸면 중복 선언이 되므로 그냥 컴파일 에러가 발생한다

 

이건 Properties에 있는 Height Fog 관련 변수들을 삭제하면 해결된다

 

물론 이렇게 되면 머티리얼에서 Height Fog 관련 프로퍼티들이 사라지므로 조정할 수 없게 된다

‘아니 그럼 소용 없는 거 아닌가?’라고 생각할 수 있는데 이걸 해결해 주는 것이 바로 Global Shader Variable!


Global Shader Variable 사용하기!

Global Shader Variable는 말 그대로 셰이더에 전역 변수를 의미한다

유니티에서는 스크립트를 통해서 모든 셰이더의 Global Shader Variable에 대해서 값을 일괄적으로 설정할 수 있다

 

아래가 샘플 코드이다

using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class HeightFogController : MonoBehaviour
{
    // Properties
    [Header("Height Fog")]
    [SerializeField] private bool HeightFogOn = false;
    [ColorUsage(true, true)] public Color HeightFogColor = Color.gray;
    [Range(0.0f, 1.0f)]public float HeightFogDensity = 0.0f;
    public float MaxFogHeight = 1.0f;
    
    [Header("Height Fog Noise")]
    public Texture HeightFogNoise = null;
    [Range(0.0f, 5.0f)]public float NoisePower = 1.0f;
    [Range(-1.0f, 1.0f)]public float NoiseScrollSpeedX = 0.0f;
    [Range(-1.0f, 1.0f)]public float NoiseScrollSpeedY = 0.0f;

    // Property ID
    private int _HeightFogNoise = -1;
    private int _HeightFogNoisePower = -1;
    private int _HeightFogNoiseSpeedX = -1;
    private int _HeightFogNoiseSpeedY = -1;
    private int _HeightFogColor = -1;
    private int _HeightFogDensity = -1;
    private int _MaxFogHeight = -1;
    
    // Global Keyword
    private GlobalKeyword HeightFogKeyword;
    
    void Awake()
    {
        _HeightFogNoise = Shader.PropertyToID("_HeightFogNoise");
        _HeightFogNoisePower = Shader.PropertyToID("_HeightFogNoisePower");
        _HeightFogNoiseSpeedX = Shader.PropertyToID("_HeightFogNoiseSpeedX");
        _HeightFogNoiseSpeedY = Shader.PropertyToID("_HeightFogNoiseSpeedY");
        _HeightFogColor = Shader.PropertyToID("_HeightFogColor");
        _HeightFogDensity = Shader.PropertyToID("_HeightFogDensity");
        _MaxFogHeight = Shader.PropertyToID("_MaxFogHeight");
        
        HeightFogKeyword = GlobalKeyword.Create("_HEIGHT_FOG");
    }

    void Update()
    {
        if (HeightFogOn)
            Shader.EnableKeyword(HeightFogKeyword);
        else
            Shader.DisableKeyword(HeightFogKeyword);
        
        Shader.SetGlobalColor(_HeightFogColor, HeightFogColor);
        Shader.SetGlobalFloat(_HeightFogDensity, (float)HeightFogDensity);
        Shader.SetGlobalFloat(_MaxFogHeight, MaxFogHeight);
        
        Shader.SetGlobalTexture(_HeightFogNoise, HeightFogNoise);
        Shader.SetGlobalFloat(_HeightFogNoisePower, NoisePower);
        Shader.SetGlobalFloat(_HeightFogNoiseSpeedX, NoiseScrollSpeedX);
        Shader.SetGlobalFloat(_HeightFogNoiseSpeedY, NoiseScrollSpeedY);
    }

    void OnDisable()
    {
        Shader.DisableKeyword(HeightFogKeyword);
    }

    void OnDestroy()
    {
        Shader.DisableKeyword(HeightFogKeyword);
    }
}

 

위의 스크립트를 적당한 오브젝트에 붙여두면 완성!


결과

이제 HeightFogController로 모든 오브젝트에 대한 Height Fog의 변수를 컨트롤할 수 있다!

물론 HeightFogLib.hlsl를 include해야 하지만


주의!) _HEIGHT_FOG_ON을 _HEIGHT_FOG로 변경한 이유

변경한 이유는 다름이 아니라 Shader.EnableKeywordShader.DisableKeyword가 평범하게 동작하지 않았기 때문이다

셰이더에서 Toggle을 붙인 프로퍼티가 있으면 자동으로 _ON가 붙은 키워드를 생성하는 것과 연관이 있을까?

자세히 조사는 하지 않아서 명확한 이유는 모르겠다…

아무튼 Global Keyword 뒤에 On, Off를 붙이는 건 피하도록 하자!


마무리

이걸로 Height Fog가 더 나은 형태로 진화했다

덤으로 Global Shader Variable에 대해서도 알게 되었으니 만족스러운 태스크였다


참고 사이트

UnityにはShaderのグローバル変数が存在する - Qiita

 

UnityにはShaderのグローバル変数が存在する - Qiita

#はじめにこんにちは、アドベントカレンダー16日目担当の避雷です。急ですがこれを見てください。https://docs.unity3d.com/ScriptReference/Shader.Se…

qiita.com