https://www.youtube.com/watch?v=rSKMYc1CQHE&pp=ygUXZmxvb2lkIHNpbXVsYXRpb24gdW5pdHk%3D
Compute Shader를 극한으로 사용한 플루이드 시뮬레이션.
쉐이더와 ECS로 온갖 최적화를 시도한 대단한 영상이다.
나도 이런거 만들어보고 싶다..
Compute Shader는 기존의 쉐이더와는 아주 다른 방식이다.
그래픽 파이프라인에도 포함되지 않는다.
Compute Shader는 GPU 상에서 임의로 코드 실행이 가능하도록 해준다.
그래픽을 목적으로 한게 아니여도 된다는 뜻이다.
이를 이용하면 병렬적으로 많은 계산을 GPU로 돌릴 수 있게 된다.
compute shader 자체만으로 아주 폭넓게 다룰 수 있는 주제이기 때문에 간단한 예시로 포스팅을 하려고 한다.
compute shader를 사용하기 위해 유니티에서 전형적으로 사용할 수 있는 워크플로가 2개 있다.
첫 번째는 CPU에서 GPU로 데이터를 전송하고, GPU에서 일부 처리를 수행한 후, CPU 측에서 결과를 읽고 해당 결과를 활용하여 작업을 수행하는 것.
두 번째는 CPU에서 GPU로 데이터를 전송하고, 컴퓨트 쉐이더를 실행한 후, CPU로 데이터를 다시 복사할 필요 없이 별도의 셰이더 내에서 결과를 읽는 것
두 번째는 CPU에게 다시 데이터를 보낼 필요가 없어 매우 유용하다.
Grass Mesh Instancing
Grass Effect는 다음 요소들을 요구한다.
- Teraain Mesh, Grass Blade Mesh
- Compute Shader에 데이터를 보내기 위해, 두 Mesh를 읽을 수 있는 C# 스크립트
- 두 메쉬의 버텍스 정보들을 받을 수 있는 Compute Shader
- 각 풀잎을 렌더링할 일반적인 쉐이더. 이 쉐이더는 compute shader에 의해 생성된 위치 정보를 vertex shader로 처리하고, fragment shader는 색상을 처리한다.

메쉬를 임포트할 때는 read/Write를 꼭 체크하자.
Procedual Grass C# Script
using UnityEngine;
public class ProceduralGrass : MonoBehaviour
{
public ComputeShader computeShader;
private Mesh terrainMesh;
public Mesh grassMesh;
public Material material;
public float scale = 0.1f;
public Vector2 minMaxBladeHeight = new Vector2(0.5f, 1.5f);
private GraphicsBuffer terrainTriangleBuffer;
private GraphicsBuffer terrainVertexBuffer;
private GraphicsBuffer transformMatrixBuffer;
private GraphicsBuffer grassTriangleBuffer;
private GraphicsBuffer grassVertexBuffer;
private GraphicsBuffer grassUVBuffer;
private Bounds bounds;
private int kernel;
private uint threadGroupSize;
private int terrainTriangleCount = 0;
private void Start()
{
kernel = computeShader.FindKernel("TerrainOffsets");
terrainMesh = GetComponent<MeshFilter>().sharedMesh;
// Terrain data for the compute shader.
Vector3[] terrainVertices = terrainMesh.vertices;
terrainVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainVertices.Length, sizeof(float) * 3);
terrainVertexBuffer.SetData(terrainVertices);
computeShader.SetBuffer(kernel, "_TerrainPositions", terrainVertexBuffer);
int[] terrainTriangles = terrainMesh.triangles;
terrainTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangles.Length, sizeof(int));
terrainTriangleBuffer.SetData(terrainTriangles);
computeShader.SetBuffer(kernel, "_TerrainTriangles", terrainTriangleBuffer);
terrainTriangleCount = terrainTriangles.Length / 3;
// Grass data for RenderPrimitives.
Vector3[] grassVertices = grassMesh.vertices;
grassVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassVertices.Length, sizeof(float) * 3);
grassVertexBuffer.SetData(grassVertices);
int[] grassTriangles = grassMesh.triangles;
grassTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassTriangles.Length, sizeof(int));
grassTriangleBuffer.SetData(grassTriangles);
Vector2[] grassUVs = grassMesh.uv;
grassUVBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassUVs.Length, sizeof(float) * 2);
grassUVBuffer.SetData(grassUVs);
transformMatrixBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangleCount, sizeof(float) * 16);
computeShader.SetBuffer(kernel, "_TransformMatrices", transformMatrixBuffer);
// Set bounds.
bounds = terrainMesh.bounds;
bounds.center += transform.position;
bounds.Expand(minMaxBladeHeight.y);
RunComputeShader();
}
private void RunComputeShader()
{
computeShader.SetMatrix("_TerrainObjectToWorld", transform.localToWorldMatrix);
computeShader.SetInt("_TerrainTriangleCount", terrainTriangleCount);
computeShader.SetVector("_MinMaxBladeHeight", minMaxBladeHeight);
computeShader.SetFloat("_Scale", scale);
computeShader.GetKernelThreadGroupSizes(kernel, out threadGroupSize, out _, out _);
int threadGroups = Mathf.CeilToInt(terrainTriangleCount / threadGroupSize);
computeShader.Dispatch(kernel, threadGroups, 1, 1);
}
private void Update()
{
RenderParams rp = new RenderParams(material);
rp.worldBounds = bounds;
rp.matProps = new MaterialPropertyBlock();
rp.matProps.SetBuffer("_TransformMatrices", transformMatrixBuffer);
rp.matProps.SetBuffer("_Positions", grassVertexBuffer);
rp.matProps.SetBuffer("_UVs", grassUVBuffer);
Graphics.RenderPrimitivesIndexed(rp, MeshTopology.Triangles, grassTriangleBuffer, grassTriangleBuffer.count, instanceCount: terrainTriangleCount);
}
private void OnDestroy()
{
terrainTriangleBuffer.Dispose();
terrainVertexBuffer.Dispose();
transformMatrixBuffer.Dispose();
grassTriangleBuffer.Dispose();
grassVertexBuffer.Dispose();
grassUVBuffer.Dispose();
}
}
코드가 좀 길다. 하나씩 독파 해보자.
선언부
public ComputeShader computeShader;
private Mesh terrainMesh;
public Mesh grassMesh;
public Material material;
public float scale = 0.1f;
public Vector2 minMaxBladeHeight = new Vector2(0.5f, 1.5f);
private GraphicsBuffer terrainTriangleBuffer;
private GraphicsBuffer terrainVertexBuffer;
private GraphicsBuffer transformMatrixBuffer;
private GraphicsBuffer grassTriangleBuffer;
private GraphicsBuffer grassVertexBuffer;
private GraphicsBuffer grassUVBuffer;
private Bounds bounds;
private int kernel;
private uint threadGroupSize;
private int terrainTriangleCount = 0;
Computeshader를 이용해서 선언하고
그 외에 GraphicsBuffer 타입은 ComputeBuffer의 다른 타입의 버퍼이다.
이 버퍼는 일반적으로 연산용으로 쓰인다.
두 버퍼의 차이점이라 하면.. GraphicsBuffer는 그래픽과 연관될 때
ComputeBuffer는 그 외 다른 용도로 쓰인다.
Bounds 타입은 오브젝트를 컬링 할 때 바운딩 박스로 사용되는 타입이다.
그외에 변수들은 함수에서 사용될때 알아보자.
Start 함수
private void Start()
{
kernel = computeShader.FindKernel("TerrainOffsets");
terrainMesh = GetComponent<MeshFilter>().sharedMesh;
// Terrain data for the compute shader.
Vector3[] terrainVertices = terrainMesh.vertices;
terrainVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainVertices.Length, sizeof(float) * 3);
terrainVertexBuffer.SetData(terrainVertices);
computeShader.SetBuffer(kernel, "_TerrainPositions", terrainVertexBuffer);
int[] terrainTriangles = terrainMesh.triangles;
terrainTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangles.Length, sizeof(int));
terrainTriangleBuffer.SetData(terrainTriangles);
computeShader.SetBuffer(kernel, "_TerrainTriangles", terrainTriangleBuffer);
terrainTriangleCount = terrainTriangles.Length / 3;
// Grass data for RenderPrimitives.
Vector3[] grassVertices = grassMesh.vertices;
grassVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassVertices.Length, sizeof(float) * 3);
grassVertexBuffer.SetData(grassVertices);
int[] grassTriangles = grassMesh.triangles;
grassTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassTriangles.Length, sizeof(int));
grassTriangleBuffer.SetData(grassTriangles);
Vector2[] grassUVs = grassMesh.uv;
grassUVBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassUVs.Length, sizeof(float) * 2);
grassUVBuffer.SetData(grassUVs);
transformMatrixBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangleCount, sizeof(float) * 16);
computeShader.SetBuffer(kernel, "_TransformMatrices", transformMatrixBuffer);
// Set bounds.
bounds = terrainMesh.bounds;
bounds.center += transform.position;
bounds.Expand(minMaxBladeHeight.y);
RunComputeShader();
}
Start 함수는 전체적으로 ComputeShader에서 사용할 데이터를 준비하는 단계라 생각하자.
(1) integer kernel = computeShader.FindKernel("TerrainOffsets");
TerrainOffsets 란 이름으로 커널을 찾아 생성한다.
이 때, 커널은 컴퓨터 쉐이더의 함수를 참조하는 값이다. 컴퓨트 쉐이더들은 이러한 커널들을 포함하고 있다.
(2) 메쉬 이해하기
메쉬는 간단히 버텍스의 리스트로 이루어져 있으며, 3차원 벡터들로 오브젝트 공간 안에서 각 버텍스의 포지션을 표현한다.

이런 버텍스 정보외에 메쉬는 UV Coords, Normals, Tangents, Colors 값들을 갖고 있다.
(3) 그래픽스 버퍼들 생성해서 넘겨주기.
terrainMesh = GetComponent<MeshFilter>().sharedMesh;
// Terrain data for the compute shader.
Vector3[] terrainVertices = terrainMesh.vertices;
terrainVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainVertices.Length, sizeof(float) * 3);
terrainVertexBuffer.SetData(terrainVertices);
computeShader.SetBuffer(kernel, "_TerrainPositions", terrainVertexBuffer);
MeshFilter로부터 버텍스를 가져와보자.
가져온 버텍스 벡터 배열은 그래픽 버퍼를 생성할 때 넣어준다.
지금 코드로는 무슨 일을 하는지 알 수 없지만
가져온 버텍스 정보들은 그래픽스 버퍼의 데이터로 넣고, computeShader에게 버퍼를 셋업 하고 있다.
지금은 그래픽스 버퍼를 생성해서 컴퓨트 쉐이더에 넣어주고 있다는것만 이해하고 나중에 보자.
Target.Structured는 컴퓨트 쉐이더에서 Structured Buffer 란 것을 이용할 때 쓰일 것이다.
int[] terrainTriangles = terrainMesh.triangles;
terrainTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangles.Length, sizeof(int));
terrainTriangleBuffer.SetData(terrainTriangles);
computeShader.SetBuffer(kernel, "_TerrainTriangles", terrainTriangleBuffer);
terrainTriangleCount = terrainTriangles.Length / 3;
보아하니 이번엔, 메쉬로부터 삼각형들을 가져온다.
마찬가지로 버퍼를 생성해서 값을 넘겨주고 있다.
// Grass data for RenderPrimitives.
Vector3[] grassVertices = grassMesh.vertices;
grassVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassVertices.Length, sizeof(float) * 3);
grassVertexBuffer.SetData(grassVertices);
int[] grassTriangles = grassMesh.triangles;
grassTriangleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassTriangles.Length, sizeof(int));
grassTriangleBuffer.SetData(grassTriangles);
Vector2[] grassUVs = grassMesh.uv;
grassUVBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, grassUVs.Length, sizeof(float) * 2);
grassUVBuffer.SetData(grassUVs);
transformMatrixBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, terrainTriangleCount, sizeof(float) * 16);
computeShader.SetBuffer(kernel, "_TransformMatrices", transformMatrixBuffer);
맵에서 할 수 있는 세팅을 끝냈으니, 다음엔 풀잎들 차례이다.
풀잎 메쉬의 버텍스, 삼각형, UV를 가져와서 그래픽스를 생성하고 넣어주고 있다.
하나씩 살펴보니 어렵지 않다.
(4) 바운드 세팅
// Set bounds.
bounds = terrainMesh.bounds;
bounds.center += transform.position;
bounds.Expand(minMaxBladeHeight.y);
RunComputeShader();
유니티에서 카메라 밖으로 오브젝트가 넘어 갈 때, 컬링한다. 이는 오브젝트의 형태는 상관없이 바운딩 박스. Bounds에 의해 결정된다. 기본적으로 유니티에서 자동으로 생성되는 값이지만..
우리는 컴퓨트 쉐이더 내에서 메쉬를 생성해서 써야 하기 때문에, 직접 정해줘야 한다.
준비가 끝났으면, RunComputeShader() 를 실행한다.
이는 유니티를 실행하는 순간 컴퓨트 쉐이더를 켜겠다는 뜻이다.
RunComputeShader 함수
private void RunComputeShader()
{
computeShader.SetMatrix("_TerrainObjectToWorld", transform.localToWorldMatrix);
computeShader.SetInt("_TerrainTriangleCount", terrainTriangleCount);
computeShader.SetVector("_MinMaxBladeHeight", minMaxBladeHeight);
computeShader.SetFloat("_Scale", scale);
computeShader.GetKernelThreadGroupSizes(kernel, out threadGroupSize, out _, out _);
int threadGroups = Mathf.CeilToInt(terrainTriangleCount / threadGroupSize);
computeShader.Dispatch(kernel, threadGroups, 1, 1);
}
아직 컴퓨트 쉐이더를 만들지 않았으니 용도는 모른다.
하지만 Properties 같은 느낌으로 데이터를 계속 주입 해주고 있는 건 짐작 갈 것이다.
버퍼는 신나게 만들었는데.. 병렬로 작업을 하려면 사이즈 명시가 중요하다.
무턱대고 수많은 워커를 만들 수는 없으니까...
우리의 GPU로부터 워크 스레드의 갯수를 가져오고. 메쉬에 기반한 삼각형들의 수를 고려해서 나온 terrainTriangleCount로 몇개의 워커를 돌릴지 명시해준다.
갑자기 데이터로더를 만들거나 CUDA 프로그래밍을 하는 기분이다.. 하지만 이 셋은 모두 정확히 병렬 계산이라는 목표가 동일하다.
Update 함수
private void Update()
{
RenderParams rp = new RenderParams(material);
rp.worldBounds = bounds;
rp.matProps = new MaterialPropertyBlock();
rp.matProps.SetBuffer("_TransformMatrices", transformMatrixBuffer);
rp.matProps.SetBuffer("_Positions", grassVertexBuffer);
rp.matProps.SetBuffer("_UVs", grassUVBuffer);
Graphics.RenderPrimitivesIndexed(rp, MeshTopology.Triangles, grassTriangleBuffer, grassTriangleBuffer.count, instanceCount: terrainTriangleCount);
}
Grass를 렌더링하는건 한번 실행하고 끝나는게 아니다.
계속해서 우리 눈에 매 프레임마다 표시를 해줘야 한다.
Graphics API를 이용해 메쉬를 계속 드로우 해줘야 한다.
GPU 인스턴싱이랑 동일한 방식으로 RenderParam을 만들고, MaterialPropertyBlock를 생성해서 렌더링 함수에 넣어준다.
OnDestroy 함수
private void OnDestroy()
{
terrainTriangleBuffer.Dispose();
terrainVertexBuffer.Dispose();
transformMatrixBuffer.Dispose();
grassTriangleBuffer.Dispose();
grassVertexBuffer.Dispose();
grassUVBuffer.Dispose();
}
버퍼들과 스레드는 계속 돌아갈텐데,
사용 용도가 없어졌다고 막상 오브젝트를 Destroy하면, GPU에서는 반응을 못할 것이다.
최적화나 메모리 누수를 위해서라면, 메모리 해제를 해줘야 한다.
언제까지 가비지 콜렉터에게 맡길 것인가? 직접 해제해서 문제의 발단을 없애자.
지금까지 보면 C# 스크립트는 2가지밖에 안했다.
컴퓨트 쉐이더를 만들어 데이터를 입력해주고, 계속 렌더링 시킨다.
그럼 이제 컴퓨트 쉐이더를 만들어보자.
ProcedualGrass Compute Shader
#pragma kernel TerrainOffsets
StructuredBuffer<int> _TerrainTriangles;
StructuredBuffer<float3> _TerrainPositions;
RWStructuredBuffer<float4x4> _TransformMatrices;
uniform int _TerrainTriangleCount;
uniform float _Scale;
uniform float2 _MinMaxBladeHeight;
uniform float4x4 _TerrainObjectToWorld;
#define TWO_PI 6.28318530718f
float randomRange(float2 seed, float min, float max)
{
float randnum = frac(sin(dot(seed, float2(12.9898, 78.233)))*43758.5453);
return lerp(min, max, randnum);
}
float4x4 rotationMatrixY(float angle)
{
float s, c;
sincos(angle, s, c);
return float4x4
(
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1
);
}
[numthreads(64, 1, 1)]
void TerrainOffsets(uint3 id : SV_DispatchThreadID)
{
if (id.x > _TerrainTriangleCount)
{
return;
}
int triStart = id.x * 3;
float3 posA = _TerrainPositions[_TerrainTriangles[triStart]];
float3 posB = _TerrainPositions[_TerrainTriangles[triStart + 1]];
float3 posC = _TerrainPositions[_TerrainTriangles[triStart + 2]];
float3 triangleCenterPos = (posA + posB + posC) / 3.0f;
float2 randomSeed1 = float2(id.x, id.y);
float2 randomSeed2 = float2(id.y, id.x);
float scaleY = _Scale * randomRange(randomSeed1, _MinMaxBladeHeight.x, _MinMaxBladeHeight.y);
float offsetX = randomRange(randomSeed1, -0.2f, 0.2f);
float offsetZ = randomRange(randomSeed2, -0.2f, 0.2f);
float4x4 grassTransformMatrix = float4x4
(
_Scale, 0, 0, triangleCenterPos.x + offsetX,
0, scaleY, 0, triangleCenterPos.y,
0, 0, _Scale, triangleCenterPos.z + offsetZ,
0, 0, 0, 1
);
float4x4 randomRotationMatrix = rotationMatrixY(randomRange(randomSeed1, 0.0f, TWO_PI));
_TransformMatrices[id.x] = mul(_TerrainObjectToWorld, mul(grassTransformMatrix, randomRotationMatrix));
}
이름만 쉐이더지, 완전 생소한 모습이다.
이번에도 정신차리고 독파하자.
전처리 및 선언부
#pragma kernel TerrainOffsets
StructuredBuffer<int> _TerrainTriangles;
StructuredBuffer<float3> _TerrainPositions;
RWStructuredBuffer<float4x4> _TransformMatrices;
uniform int _TerrainTriangleCount;
uniform float _Scale;
uniform float2 _MinMaxBladeHeight;
uniform float4x4 _TerrainObjectToWorld;
#define TWO_PI 6.28318530718f
precompiler는 kernel을 사용하고 있다. TerrainOffsets라는 이름으로 커널을 명시해서 C#에서 이를 읽게 만들어준다.
StructuredBuffer는 구조화된 데이터를 저장하고 GPU에 접근할 수 있게 해주는 버퍼이다.
선언부에 있는 모든 버퍼와 변수들은 스크립트에서 주입받은 값들이다.
randomRange 함수
float randomRange(float2 seed, float min, float max)
{
float randnum = frac(sin(dot(seed, float2(12.9898, 78.233)))*43758.5453);
return lerp(min, max, randnum);
}
함수 이름 그대로 시드에 따라 랜덤으로 값을 정해준다.
아직 용도를 모르지만, 무언갈 랜덤화 시킬 것으로 예상한다.
rotationMatrixY 함수
float4x4 rotationMatrixY(float angle)
{
float s, c;
sincos(angle, s, c);
return float4x4
(
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1
);
}
말이 쉐이더지 사실상 빈 껍데기에서 코드를 짜는것과 같다.
약간 리눅스에서 Sh 파일을 만드는 기분이다.
회전 함수도 직접 구현해준다.
TerrainOffsets 함수
[numthreads(64, 1, 1)]
void TerrainOffsets(uint3 id : SV_DispatchThreadID)
{
if (id.x > _TerrainTriangleCount)
{
return;
}
int triStart = id.x * 3;
float3 posA = _TerrainPositions[_TerrainTriangles[triStart]];
float3 posB = _TerrainPositions[_TerrainTriangles[triStart + 1]];
float3 posC = _TerrainPositions[_TerrainTriangles[triStart + 2]];
float3 triangleCenterPos = (posA + posB + posC) / 3.0f;
float2 randomSeed1 = float2(id.x, id.y);
float2 randomSeed2 = float2(id.y, id.x);
float scaleY = _Scale * randomRange(randomSeed1, _MinMaxBladeHeight.x, _MinMaxBladeHeight.y);
float offsetX = randomRange(randomSeed1, -0.2f, 0.2f);
float offsetZ = randomRange(randomSeed2, -0.2f, 0.2f);
float4x4 grassTransformMatrix = float4x4
(
_Scale, 0, 0, triangleCenterPos.x + offsetX,
0, scaleY, 0, triangleCenterPos.y,
0, 0, _Scale, triangleCenterPos.z + offsetZ,
0, 0, 0, 1
);
float4x4 randomRotationMatrix = rotationMatrixY(randomRange(randomSeed1, 0.0f, TWO_PI));
_TransformMatrices[id.x] = mul(_TerrainObjectToWorld, mul(grassTransformMatrix, randomRotationMatrix));
}
(1) numthreads(X, Y, Z)
numthreads 어트리뷰트는 스레드 그룹의 사이즈를 3차원화 시켜서 수를 명시한다.
64, 1, 1 이면 X축으로 64개만큼의 스레드를 사용 할 것이다란 뜻이다.
(2) C#으로부터 Dispatch된 정보들은 ID화 되서 입력 파라미터로 들어온다.
uint3면 네트워크에서도 그렇고, ID로 자주 사용되는 타입이다.
(3) 삼각형을 이용하고 있으므로 각 포지션 값은 3배수로 구해진다.
(4) 랜덤 시드를 이용한 스케일 임의로 결정하기.
딱히 설명은 필요 없을 것 같다. Scale은 랜덤 시드에 의해 무작위로 정해진다.
(5) 행렬 이용하기.
Transform을 원활하게 하기 위해 사원수 형태를 이용하는 것 같다.
이번에도 무작위로 회전 시키기 위해 Random 함수를 이용한다.
이게 끝?
잘 생각해보면 컴퓨트 쉐이더가 한일은
Position이 정해지고, Scale이 정해지고, Rotation을 진행했다.
Transform 관련 계산을 전부 담당한 것이다.
그 결과는 고작 _TransformMatrices 하나 뿐이다.
이 정보들은 모두 처리 되었으면 이제 무엇을 하는걸까?
Shader 에서 살펴보자.
Grass Shader
Shader "LucidBoundary/Grass"
{
Properties
{
_BaseColor("Base Color", Color) = (0, 0, 0, 1)
_TipColor("Tip Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"Queue" = "Geometry"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct v2f
{
float4 positionCS : SV_Position;
float2 uv : TEXCOORD0;
};
StructuredBuffer<float3> _Positions;
StructuredBuffer<float2> _UVs;
StructuredBuffer<float4x4> _TransformMatrices;
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _TipColor;
CBUFFER_END
v2f vert (uint vertexID : SV_VertexID, uint instanceID : SV_InstanceID)
{
float4x4 mat = _TransformMatrices[instanceID];
v2f o;
float4 pos = float4(_Positions[vertexID], 1.0f);
pos = mul(mat, pos);
o.positionCS = mul(UNITY_MATRIX_VP, pos);
o.uv = _UVs[vertexID];
return o;
}
float4 frag (v2f i) : SV_Target
{
return lerp(_BaseColor, _TipColor, i.uv.y);
}
ENDHLSL
}
}
Fallback Off
}
별로 대단한건 안보이지만, 버퍼들이 추가 된 것을 알 수 있다.
StructuredBuffer<float3> _Positions;
StructuredBuffer<float2> _UVs;
StructuredBuffer<float4x4> _TransformMatrices;
이 값들은 모두 스크립트의 Update문에서 활용중이다.
_TransformMatrices 는 컴퓨트 쉐이더에서 나온 결과물이기도 하다.
vert, frag는 단순하여 설명할 필요도 없을 것 같다.

너무 잘 돌아가는 모습이다.
마치며
Compute Shader 의외로 사용법은 간단하다.
잘 활용이 중요한듯...
'Game Dev > Unity Shader' 카테고리의 다른 글
| Sprite Appear / Disappear Shader - 유니티 Shader Graph (0) | 2025.11.25 |
|---|---|
| Toon Shader 만들기 - HLSL 유니티 Shader (0) | 2025.10.29 |
| 21. Geometry Shader - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.27 |
| 20. Tessellation Shader - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.26 |
| 19. Image Effects, Post-Processing Effects - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.24 |