커스텀 하늘 만들기
고해상도 렌더 파이프라인(HDRP)은 하늘을 조명 파이프라인과 일관되게 유지하면서 스카이 시스템을 사용하여 자체 프로퍼티와 셰이더로 커스텀 하늘을 만들 수 있습니다.
자체 하늘을 만들려면 다음 사항을 처리하기 위해 몇 가지 스크립트를 만듭니다.
하늘 렌더러 사용
위의 모든 단계를 완료하면 Unity 프로젝트의 볼륨에 대한 시각 환경 오버라이드에 있는 Sky Type 드롭다운에 새로운 하늘이 자동으로 나타납니다.
하늘 설정
먼저 SkySettings에서 상속하는 새로운 클래스를 만듭니다. 이 새로운 클래스에는 원하는 특정 하늘 렌더러와 관련된 모든 프로퍼티가 포함됩니다.
이 클래스에는 다음 사항이 반드시 포함되어야 합니다.
- SkyUniqueID 속성: 이 속성은 반드시 이 특정 하늘에 고유한 정수여야 합니다. 즉 이 속성은 다른 SkySettings와 충돌해선 안 됩니다. SkyType 열거형을 사용하여 HDRP가 이미 사용하는 값을 확인합니다.
- GetHashCode: 스카이 시스템은 이 함수를 사용하여 하늘 반사 큐브맵을 다시 렌더링하는 시기를 결정합니다.
- GetSkyRendererType: 스카이 시스템은 이 함수를 사용하여 적절한 렌더러를 인스턴스화합니다.
예를 들어 SkySettings의 HDRI 하늘 구현은 다음과 같습니다.
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
[VolumeComponentMenu("Sky/New Sky")]
// SkyUniqueID does not need to be part of built-in HDRP SkyType enumeration.
// This is only provided to track IDs used by HDRP natively.
// You can use any integer value.
[SkyUniqueID(NEW_SKY_UNIQUE_ID)]
public class NewSky : SkySettings
{
const int NEW_SKY_UNIQUE_ID = 20382390;
[Tooltip("Specify the cubemap HDRP uses to render the sky.")]
public CubemapParameter hdriSky = new CubemapParameter(null);
public override Type GetSkyRendererType()
{
return typeof(NewSkyRenderer);
}
public override int GetHashCode()
{
int hash = base.GetHashCode();
unchecked
{
hash = hdriSky.value != null ? hash * 23 + hdriSky.GetHashCode() : hash;
}
return hash;
}
public override int GetHashCode(Camera camera)
{
// Implement if your sky depends on the camera settings (like position for instance)
return GetHashCode();
}
}
하늘 렌더러
이제 실제로 하늘을 렌더링하는 클래스를 조명용 큐브맵으로 만들거나 배경에 대해 시각적으로 만들어야 합니다. 이 곳에서 반드시 특정 렌더링 기능을 구현해야 합니다.
SkyRenderer는 반드시 다음과 같이 SkyRenderer 인터페이스를 구현해야 합니다.
public abstract class SkyRenderer
{
int m_LastFrameUpdate = -1;
/// <summary>
/// Called on startup. Create resources used by the renderer (shaders, materials, etc).
/// </summary>
public abstract void Build();
/// <summary>
/// Called on cleanup. Release resources used by the renderer.
/// </summary>
public abstract void Cleanup();
/// <summary>
/// HDRP calls this function once every frame. Implement it if your SkyRenderer needs to iterate independently of the user defined update frequency (see SkySettings UpdateMode).
/// </summary>
/// <returns>True if the update determines that sky lighting needs to be re-rendered. False otherwise.</returns>
protected virtual bool Update(BuiltinSkyParameters builtinParams) { return false; }
/// <summary>
/// Implements actual rendering of the sky. HDRP calls this when rendering the sky into a cubemap (for lighting) and also during main frame rendering.
/// </summary>
/// <param name="builtinParams">Engine parameters that you can use to render the sky.</param>
/// <param name="renderForCubemap">Pass in true if you want to render the sky into a cubemap for lighting. This is useful when the sky renderer needs a different implementation in this case.</param>
/// <param name="renderSunDisk">If the sky renderer supports the rendering of a sun disk, it must not render it if this is set to false.</param>
public abstract void RenderSky(BuiltinSkyParameters builtinParams, bool renderForCubemap, bool renderSunDisk);
예를 들어 SkyRenderer를 간단히 구현하면 다음과 같습니다.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
class NewSkyRenderer : SkyRenderer
{
public static readonly int _Cubemap = Shader.PropertyToID("_Cubemap");
public static readonly int _SkyParam = Shader.PropertyToID("_SkyParam");
public static readonly int _PixelCoordToViewDirWS = Shader.PropertyToID("_PixelCoordToViewDirWS");
Material m_NewSkyMaterial; // Renders a cubemap into a render texture (can be cube or 2D)
MaterialPropertyBlock m_PropertyBlock = new MaterialPropertyBlock();
private static int m_RenderCubemapID = 0; // FragBaking
private static int m_RenderFullscreenSkyID = 1; // FragRender
public override void Build()
{
m_NewSkyMaterial = CoreUtils.CreateEngineMaterial(GetNewSkyShader());
}
// Project dependent way to retrieve a shader.
Shader GetNewSkyShader()
{
// Implement me
return null;
}
public override void Cleanup()
{
CoreUtils.Destroy(m_NewSkyMaterial);
}
protected override bool Update(BuiltinSkyParameters builtinParams)
{
return false;
}
public override void RenderSky(BuiltinSkyParameters builtinParams, bool renderForCubemap, bool renderSunDisk)
{
using (new ProfilingSample(builtinParams.commandBuffer, "Draw sky"))
{
var newSky = builtinParams.skySettings as NewSky;
int passID = renderForCubemap ? m_RenderCubemapID : m_RenderFullscreenSkyID;
float intensity = GetSkyIntensity(newSky, builtinParams.debugSettings);
float phi = -Mathf.Deg2Rad * newSky.rotation.value; // -rotation to match Legacy
m_PropertyBlock.SetTexture(_Cubemap, newSky.hdriSky.value);
m_PropertyBlock.SetVector(_SkyParam, new Vector4(intensity, 0.0f, Mathf.Cos(phi), Mathf.Sin(phi)));
m_PropertyBlock.SetMatrix(_PixelCoordToViewDirWS, builtinParams.pixelCoordToViewDirMatrix);
CoreUtils.DrawFullScreen(builtinParams.commandBuffer, m_NewSkyMaterial, m_PropertyBlock, passID);
}
}
}
중요한 참조 사항:
하늘 렌더러가 대용량 데이터(미리 계산된 텍스처나 유사한 것처럼)를 관리해야 하는 경우 특별히 주의를 기울여야 합니다. 실제로 렌더러의 한 인스턴스가 카메라별로 존재하여 기본적으로 이 데이터가 해당 렌더러의 멤버인 경우 이 인스턴스도 메모리에 복제됩니다. 각 하늘 렌더러가 매우 다른 요구 사항을 가질 수 있으므로 이런 종류의 데이터를 공유하는 책임은 해당 렌더러에게 있으며 사용자가 구현해야 합니다.
하늘 렌더링 셰이더
마지막으로 하늘에 대한 셰이더를 실제로 만들어야 합니다. 이 셰이더의 콘텐츠는 포함하고자 하는 효과에 따라 다릅니다.
예를 들어 SkyRenderer의 HDRI 하늘 구현은 다음과 같습니다.
Shader "Hidden/HDRP/Sky/NewSky"
{
HLSLINCLUDE
#pragma vertex Vert
#pragma editor_sync_compilation
#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Sky/SkyUtils.hlsl"
TEXTURECUBE(_Cubemap);
SAMPLER(sampler_Cubemap);
float4 _SkyParam; // x exposure, y multiplier, zw rotation (cosPhi and sinPhi)
#define _Intensity _SkyParam.x
#define _CosPhi _SkyParam.z
#define _SinPhi _SkyParam.w
#define _CosSinPhi _SkyParam.zw
struct Attributes
{
uint vertexID : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID, UNITY_RAW_FAR_CLIP_VALUE);
return output;
}
float3 RotationUp(float3 p, float2 cos_sin)
{
float3 rotDirX = float3(cos_sin.x, 0, -cos_sin.y);
float3 rotDirY = float3(cos_sin.y, 0, cos_sin.x);
return float3(dot(rotDirX, p), p.y, dot(rotDirY, p));
}
float4 GetColorWithRotation(float3 dir, float exposure, float2 cos_sin)
{
dir = RotationUp(dir, cos_sin);
float3 skyColor = SAMPLE_TEXTURECUBE_LOD(_Cubemap, sampler_Cubemap, dir, 0).rgb * _Intensity * exposure;
skyColor = ClampToFloat16Max(skyColor);
return float4(skyColor, 1.0);
}
float4 RenderSky(Varyings input, float exposure)
{
float3 viewDirWS = GetSkyViewDirWS(input.positionCS.xy);
// Reverse it to point into the scene
float3 dir = -viewDirWS;
return GetColorWithRotation(dir, exposure, _CosSinPhi);
}
float4 FragBaking(Varyings input) : SV_Target
{
return RenderSky(input, 1.0);
}
float4 FragRender(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
return RenderSky(input, GetCurrentExposureMultiplier());
}
ENDHLSL
SubShader
{
// Regular New Sky
// For cubemap
Pass
{
ZWrite Off
ZTest Always
Blend Off
Cull Off
HLSLPROGRAM
#pragma fragment FragBaking
ENDHLSL
}
// For fullscreen Sky
Pass
{
ZWrite Off
ZTest LEqual
Blend Off
Cull Off
HLSLPROGRAM
#pragma fragment FragRender
ENDHLSL
}
}
Fallback Off
}
참고: NewSky 예시는 두 가지 패스를 사용합니다. 하나는 배경에 하늘을 렌더링하기 위한 뎁스 테스트를 사용하는(지오메트리가 올바르게 하늘을 가리도록) 패스이며 다른 하나는 뎁스 테스트를 사용하지 않고 반사 큐브맵에 하늘을 렌더링하는 패스입니다.