RenderGraph에서 Shader Global 파라미터를 설정하는 방법
샘플 코드
ShaderShader "Hidden/Sample/DataTransfer"
{
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
ZTest Always ZWrite Off Cull Off
Pass // 색상, 상하 반전 텍스처 그리기
{
Name "DrawNegative"
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
#pragma vertex Vert
#pragma fragment Frag
half4 Frag(Varyings input) : SV_TARGET
{
float2 uv = input.texcoord.xy;
uv.y = 1 - uv.y; // 위아래로 돌리기
half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half4 negative = half4(1 - color.rgb, color.a); // 색상 반전
return negative;
}
ENDHLSL
}
Pass // 카메라 색상과 합성
{
Name "Combine"
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
#pragma vertex Vert
#pragma fragment Frag
TEXTURE2D_X(_NegativeTexture);
float4 _Params;
#define CENTER_UV _Params.xy
#define RADIUS _Params.z
#define ASPECT_RATIO _Params.w
half4 Frag(Varyings input) : SV_TARGET
{
float2 uv = input.texcoord.xy;
float2 uvDiff = uv - CENTER_UV;
uvDiff.x *= ASPECT_RATIO;
float distSqr = dot(uvDiff, uvDiff);
float radiusSqr = RADIUS * RADIUS;
half4 negativeColor = SAMPLE_TEXTURE2D_X(_NegativeTexture, sampler_LinearClamp, uv);
half4 sceneColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
// 원 안의 부분을 반전 색상으로 만들기
half inCircle = distSqr < radiusSqr;
half4 color = inCircle ? negativeColor : sceneColor;
return color;
}
ENDHLSL
}
}
}
RenderPass 및 RendererFeature
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
public class DrawNegativePass : ScriptableRenderPass
{
private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");
private static readonly int ParamsPropertyId = Shader.PropertyToID("_Params");
private Material _material;
private Vector2 _center;
private float _radius;
public void SetData(Material material, Vector2 center, float radius)
{
_material = material;
_center = center;
_radius = radius;
}
private class PassData
{
internal Material Material;
internal TextureHandle SourceTexture;
//internal TextureHandle NegativeTexture;
internal Vector4 Params;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var cameraData = frameData.Get<UniversalCameraData>();
var resourceData = frameData.Get<UniversalResourceData>();
var sourceTextureHandle = resourceData.activeColorTexture;
var aspectRatio = cameraData.cameraTargetDescriptor.width / (float)cameraData.cameraTargetDescriptor.height;
var negativeDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
negativeDescriptor.name = "_NegativeTexture";
negativeDescriptor.clearBuffer = false;
negativeDescriptor.msaaSamples = MSAASamples.None;
negativeDescriptor.depthBufferBits = 0;
var negativeTextureHandle = renderGraph.CreateTexture(negativeDescriptor);
// 카메라 컬러를 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
using (var builder = renderGraph.AddRasterRenderPass<PassData>("DrawNegativePass", out var passData))
{
// passData에 필요한 데이터 입력
passData.Material = _material;
passData.SourceTexture = sourceTextureHandle;
//passData.NegativeTexture = negativeTextureHandle;
passData.Params = new Vector4(_center.x, _center.y, _radius, aspectRatio);
// 드로잉 타겟 설정
builder.SetRenderAttachment(negativeTextureHandle, 0, AccessFlags.Write);
builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
// 해설 *1
// Shader의 Global 변수에 대한 설정이 가능하도록 하기 위해
// 주의하세요!
builder.AllowGlobalStateModification(true);
// 해설 *2
// negativeTextureHandle이 그려진 후 "_NegativeTexture"라는 이름의 GlobalTexture로 설정한다.
builder.SetGlobalTextureAfterPass(negativeTextureHandle, NegativeTexturePropertyId);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
var cmd = context.cmd;
var material = data.Material;
var source = data.SourceTexture;
var parameters = data.Params;
Blitter.BlitTexture(cmd, source, Vector2.one, material, 0);
// 해설 *3
// Texture 이외의 변수는 그리기 함수 내에서 CommandBuffer를 사용하여 Global로 설정한다.
cmd.SetGlobalVector(ParamsPropertyId, parameters);
// 해설 *4
// Texture에서도 그리기 함수 내에서 CommandBuffer를 사용하여 Global로 설정할 수 있지만, RenderGraphViewer에서 감지하지 못하기 때문에 비추천
//cmd.SetGlobalTexture(NegativeTexturePropertyId, data.NegativeTexture);
});
}
}
}
public class CombinePass : ScriptableRenderPass
{
private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");
private Material _material;
public void SetData(Material material)
{
_material = material;
}
private class PassData
{
internal Material Material;
internal TextureHandle SourceTexture;
internal Vector4 Params;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var resourceData = frameData.Get<UniversalResourceData>();
var sourceTextureHandle = resourceData.activeColorTexture;
// 현재 활성화된 카메라 컬러를 기반으로 합성 텍스처를 생성합니다.
var targetDesc = renderGraph.GetTextureDesc(sourceTextureHandle);
targetDesc.name = "_CombineTexture";
targetDesc.clearBuffer = false;
targetDesc.depthBufferBits = 0;
var combineTextureHandle = renderGraph.CreateTexture(targetDesc);
// 합성한 텍스처를 카메라 컬러로 설정합니다.
resourceData.cameraColor = combineTextureHandle;
// 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
{
// passData에 필요한 데이터 입력
passData.Material = _material;
passData.SourceTexture = sourceTextureHandle;
// 드로잉 타겟 설정
builder.SetRenderAttachment(combineTextureHandle, 0, AccessFlags.Write);
builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
// 해설 *5
// GlobalTexture 사용 선언
builder.UseGlobalTexture(NegativeTexturePropertyId, AccessFlags.Read);
// 해설 *6
// 모든 GlobalTexture 사용 신청
// builder.UseAllGlobalTexture(true);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
var cmd = context.cmd;
var material = data.Material;
var source = data.SourceTexture;
// Params와 NegativeTexture가 이전 Pass에서 Global 파라미터로 설정되었기 때문에
// 이 패스로 머티리얼을 설정하고 싶어도 그대로 사용할 수 있습니다.
Blitter.BlitTexture(cmd, source, Vector2.one, material, 1);
});
}
}
}
public class DataTransferRendererFeature : ScriptableRendererFeature
{
[SerializeField]
private Vector2 center = new(0.5f, 0.5f);
[SerializeField]
private float radius = 0.5f;
private const string ShaderPath = "Hidden/Sample/DataTransfer";
private Material _material;
private Material material
{
get
{
if (_material == null)
{
_material = CoreUtils.CreateEngineMaterial(ShaderPath);
}
return _material;
}
}
private DrawNegativePass _drawNegativePass;
private CombinePass _combinePass;
public override void Create()
{
_drawNegativePass = new DrawNegativePass
{
renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
};
_combinePass = new CombinePass
{
renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
};
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (renderingData.cameraData.cameraType == CameraType.Preview)
{
// Preview 카메라는 효과 대상에서 제외되므로 제외
return;
}
_drawNegativePass.SetData(material, center, radius);
_combinePass.SetData(material);
renderer.EnqueuePass(_drawNegativePass);
renderer.EnqueuePass(_combinePass);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
CoreUtils.Destroy(material);
}
}
}
코드 설명
- RenderGraph에서 데이터를 Global로 설정하기 전에 먼저 builder.AllowGlobalStateModification(true) 로 GlobalState 수정 권한을 얻어야 한다.그렇지 않으면 SetGlobalXXX 행위를 할 때 오류가 발생한다.
- builder.SetGlobalTextureAfterPass(textureHanlde, propertyId); 를 실행하면, 이 Pass가 실행 종료 시점에 textureHanlde가 자동으로 propertyId라는 Global Slot에 설정된다.
- Texture 타입 이외의 데이터는 기존 시스템과 비슷하게 드로잉 함수 내에서 cmd.SetGlobalXXX로 Globalize하는 RenderGraphViewer에서 확인 가능
- GlobalTexture도 사용 신청이 필요하며, 신청을 하지 않으면 Pass가 실행되는 시점에 Texture가 파기되는 경우가 있다.
- Texture 타입의 데이터도 드로잉 함수 내에서 cmd.SetGlobalTexture를 사용하여 Global화할 수 있지만, RenderGraphViewer에서 감지하지 못하고 예기치 않은 오류가 발생할 수 있으므로 권장하지 않는다.특히 Attachment에 설정된 Texture에 대해 사용하면 반드시 오류가 발생한다.
- RenderGraph는 드로잉 단계에 들어가기 전에 모든 드로잉 패스가 신청한 텍스처를 체크하여 각 텍스처의 폐기 시점을 결정하고, 텍스처는 후속 패스에서 더 이상 사용되지 않는 시점에 폐기된다(RenderGraph는 Shader 내의사용을 사전에 감지할 수 없으므로, 사전 사용 신청으로 파기 타이밍을 결정한다).그림과 같이 GlobalTexture의 사용 신청을 하지 않으면 반전 텍스처가 파기되어 획득할 수 없게 된다.
- 사용하고자 하는 GlobalTexture가 많아 일일이 작성하는 번거로움을 줄이고 싶거나, 코드의 가독성과 유지보수성을 높이고 싶다면 builder.UseAllGlobalTextures(true)로 모든 Global Texture에 대해 사용 신청을 할 수도 있다.
- 하지만 그렇게 되면 실제로 사용하지 않았는데도 사용 예정으로 판단되어 텍스처 폐기 타이밍이 늦어져 드로잉 성능에 악영향을 끼칠 수 있다.
- 다만, URP 내부에서도 builder.UseAllGlobalTextures(true) 가 많이 사용되기 때문에 큰 악영향은 없을 것으로 예상한다.
주의
AllowGlobalStateModification에 대한 공식적인 주의 의견은 다음과 같습니다.
Allow commands in the command buffer to modify global state.
This will introduce a render graph sync-point in the frame and cause all passes after this pass to never be reordered before this pass.
This may nave negative impact on performance and memory use if not used carefully so it is recommended to only allow this in specific use cases.
즉, Pass에 Global 파라미터 변경을 허용하면 해당 Pass가 실행되는 시점에 RenderGraph의 동기화 포인트(CPU와 GPU 간 데이터 전송 포인트)가 삽입되어 성능 및 메모리 사용량에 악영향을 미칠 수 있습니다.있습니다.꼭 필요한 경우에만 Global 파라미터화를 사용하세요.
RenderGraph에서 데이터 컨테이너를 사용하는 방법
RenderGraph는 ContextContainer라는 매우 유용한 컨테이너 클래스를 제공합니다.
ContextContainer의 인스턴스가 Renderer 측에 있으며, RecordRenderGraph 함수의 두 번째 인수로 Pass에 전달되어 그리기 순서대로 각 Pass에 전달됩니다.
public virtual void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
그 ContextContainer에는 URP 내부의 많은 데이터가 저장되어 있으며, 자체 제작한 Pass로 가져와서 사용할 수 있다.또한, 임의의 시점에 자체 데이터를 추가할 수도 있습니다.
사용방법 목록
데이터 수집 및 추가는 다음과 같은 기능을 통해 가능합니다.// 컨테이너 변수 이름을 frameData로 설정한다.
ContextContainer frameData;
// <DataType> 타입의 데이터 존재 여부 확인
var isContains = frameData.Contains<DataType>();
// <DataType> 타입의 데이터를 가져옵니다.
// * 존재하지 않는 경우 오류가 발생함
var data0 = frameData.Get<DataType>();
// <DataType> 타입의 데이터를 새로 생성합니다.
// * 이미 존재하는 경우 오류가 발생합니다.
var data1 = frameData.Create<DataType>();
// <DataType> 타입의 데이터를 가져옵니다.
// * 존재하지 않는 경우 새로 생성하기
var data2 = frameData.GetOrCreate<DataType>();
URP가 제공한 데이터는 이전 기사에서도 언급했듯이 주로 다음과 같다.
// 카메라 관련 정보: 변환 매트릭스, 카메라 설정 등
var cameraData = frameData.Get<UniversalCameraData>();
// 리소스 관련 정보: 카메라 컬러 버퍼, 카메라 심도 버퍼 등
var resourceData = frameData.Get<UniversalResourceData>();
// 렌더링 관련 정보: RenderCullingResults, RenderLayerMask 등
var renderingData = frameData.Get<UniversalRenderingData>();
// 조명 관련 정보: MainLight, AdditionalLights에 대한 정보 등
var lightData = frameData.Get<UniversalLightData>();
샘플 코드
앞 절의 샘플 데이터 전달 방식을 컨테이너를 사용하는 것으로 변경하면 다음과 같습니다.
- RendererFeature와 Shader는 변경 사항이 없으므로 생략합니다.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
// 해설 *1
// ContextItem 상속
public class NegativeCircleData : ContextItem
{
// 저장할 데이터(모든 유형 가능)
public TextureHandle NegativeTexture;
public Vector4 NegativeParams;
// 프레임 종료 시 리셋 처리
public override void Reset()
{
// TextureHandle은 매 프레임마다 리셋해야 한다.
NegativeTexture = TextureHandle.nullHandle;
}
}
public class DrawNegativePass : ScriptableRenderPass
{
private Material _material;
private Vector2 _center;
private float _radius;
public void SetData(Material material, Vector2 center, float radius)
{
_material = material;
_center = center;
_radius = radius;
}
private class PassData
{
internal Material Material;
internal TextureHandle SourceTexture;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var cameraData = frameData.Get<UniversalCameraData>();
var resourceData = frameData.Get<UniversalResourceData>();
var sourceTextureHandle = resourceData.activeColorTexture;
var aspectRatio = cameraData.cameraTargetDescriptor.width / (float)cameraData.cameraTargetDescriptor.height;
var negativeDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
negativeDescriptor.name = "_NegativeTexture";
negativeDescriptor.clearBuffer = false;
negativeDescriptor.depthBufferBits = 0;
var negativeTextureHandle = renderGraph.CreateTexture(negativeDescriptor);
// 해설 *2
// frameDataにNegativeCircleDataを作成する
// 중복 생성 우려가 있는 경우 frameData.GetOrCreate를 사용한다.
var negativeCircleData = frameData.Create<NegativeCircleData>();
// 다음 Pass에 전달할 데이터를 넣는다.
negativeCircleData.NegativeParams = new Vector4(_center.x, _center.y, _radius, aspectRatio);
negativeCircleData.NegativeTexture = negativeTextureHandle;
// 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
using (var builder = renderGraph.AddRasterRenderPass<PassData>("DrawNegativePass", out var passData))
{
// passData에 필요한 데이터 입력
passData.Material = _material;
passData.SourceTexture = sourceTextureHandle;
// 드로잉 타겟 설정
builder.SetRenderAttachment(negativeTextureHandle, 0, AccessFlags.Write);
builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
var cmd = context.cmd;
var material = data.Material;
var source = data.SourceTexture;
Blitter.BlitTexture(cmd, source, Vector2.one, material, 0);
});
}
}
}
public class CombinePass : ScriptableRenderPass
{
private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");
private static readonly int ParamsPropertyId = Shader.PropertyToID("_Params");
private Material _material;
public void SetData(Material material)
{
_material = material;
}
private class PassData
{
internal Material Material;
internal TextureHandle SourceTexture;
internal TextureHandle NegativeTexture;
internal Vector4 NegativeParams;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var resourceData = frameData.Get<UniversalResourceData>();
var sourceTextureHandle = resourceData.activeColorTexture;
// 해설 *3
if (!frameData.Contains<NegativeCircleData>())
{
// NegativeCircleData가 없으면 이 Pass를 실행하지 않습니다.
Debug.LogError("NegativeCircleData is not found.");
return;
}
// frameData에서 NegativeCircleData 가져오기
var negativeCircleData = frameData.Get<NegativeCircleData>();
// 현재 활성화된 카메라 컬러를 기반으로 합성 텍스처를 생성합니다.
var targetDesc = renderGraph.GetTextureDesc(sourceTextureHandle);
targetDesc.name = "_CombineTexture";
targetDesc.clearBuffer = false;
targetDesc.depthBufferBits = 0;
var combineTextureHandle = renderGraph.CreateTexture(targetDesc);
// 합성한 텍스처를 카메라 컬러로 설정합니다.
resourceData.cameraColor = combineTextureHandle;
// 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
{
// passData에 필요한 데이터 입력
passData.Material = _material;
passData.SourceTexture = sourceTextureHandle;
passData.NegativeTexture = negativeCircleData.NegativeTexture;
passData.NegativeParams = negativeCircleData.NegativeParams;
// 드로잉 타겟 설정
builder.SetRenderAttachment(combineTextureHandle, 0, AccessFlags.Write);
builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
// 해설 *4
// NegativeTexture는 물론 사용 신청이 필요합니다.
builder.UseTexture(negativeCircleData.NegativeTexture, AccessFlags.Read);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
var cmd = context.cmd;
var material = data.Material;
var source = data.SourceTexture;
var negativeTexture = data.NegativeTexture;
var negativeParams = data.NegativeParams;
// 필요한 데이터가 Global화되어 있지 않으므로 여기서 Material에 텍스처와 파라미터를 설정한다.
material.SetTexture(NegativeTexturePropertyId, negativeTexture);
material.SetVector(ParamsPropertyId, negativeParams);
Blitter.BlitTexture(cmd, source, Vector2.one, material, 1);
});
}
}
}
코드 설명
- ContextContainer에 넣을 데이터 클래스는 반드시 ContextItem을 상속해야 한다.
- Reset()은 반드시 오버라이드해야 하며, 매 프레임마다 리셋 작업을 그 안에 기술한다.
- 같은 데이터 타입으로 Create()를 두 번 이상 호출하면 에러가 발생하므로, 신규 데이터 생성(메모리 확보) 타이밍을 엄격하게 제어할 필요가 없다면 GetOrCreate()를 사용하는 것이 무난할 것 같다.
- NegativeCircleData가 존재하지 않는 경우 후속 처리 건너뛰기
- CombinePass의 실행은 DrawNegativePass의 실행 결과에 의존하지만, 직접 선행 Pass의 실행 상태를 확인하는 것이 아니라 컨테이너 내 필요한 데이터의 유무로 실행 여부를 판단하여 선행 조건이 되는 Pass에 대한 직접적인 참조를 피한다.
- 이를 통해 패스 간의 밀집 결합을 방지할 수 있다.
- 간과하기 쉽지만, 사용하는 RenderTexture는 출처와 상관없이 사용 신청을 해야 합니다.