Notice
Recent Posts
Recent Comments
Link
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

MOONSUN

[그래픽스] Domain Warping 기반 Somoke 패턴 구현 본문

D3D

[그래픽스] Domain Warping 기반 Somoke 패턴 구현

MoonSun_v 2025. 12. 18. 12:23

 

연기는 단순한 텍스처 애니메이션으로는 잘 표현되지 않는다.

실제 연기는 형태가 끊임없이 변하고, 흐르고, 말리며, 사라진다 등.. 의 움직임이 있다. 

 

이번 글에서는 이전 글에서 알아보았던 Noise → FBM → Domain Warping 구조를 기반으로,
텍스처 없이 연기의 움직임을 표현한 Procedural Smoke Pixel Shader 구현 과정을 정리해보려고 한다.

 

 

Domain Warping의 핵심 아이디어

노이즈로 좌표를 왜곡하고,
왜곡된 좌표에서 다시 노이즈를 샘플링한다.

 

이 구조를 토대로 구현해보았다. 

 

 

 

0. 개요

텍스처나 시뮬레이션 데이터를 사용하지 않고,

수학적 함수(Noise)와 시간(Time) 만으로 연기의 형태와 움직임을 생성하는 것을 목표로 했다.

 

핵심 아이디어는 다음과 같은 흐름으로 구성했다. 

Noise → FBM → Domain Warping → Flow(Advection) → Height Fade

 

 

목표 

  정적인 사각형 화면 위에 실제 연기처럼

  1. 위로 상승하고
  2. 좌우로 흔들리며
  3. 위로 갈수록 얇아지는
  4. 자연스러운 연기 패턴을 시각화한다. 

 

 

 

1. Pixel Shader 흐름

1-1. Hash 함수 – 난수의 시작점

2D좌표 p를 입력으로 받아서 0~1 범위의 의사 난수 값을 생성한다. 

float Hash(float2 p)
{
    return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}

 

GPU에는 진짜 랜덤 함수가 없기 때문에, 좌표 기반으로 항상 동일한 난수를 만들어내는 함수가 필요했다. 

  1. dot()으로 좌표를 하나의 값으로 압축
  2. sin()과 큰 상수를 곱해 난수처럼 보이게 변형
  3. frac()으로 소수 부분만 남겨 0~1 범위로 제한
이 Hash 함수는 Noise, FBM, Domain Warping의 기초 재료 역할을 한다.

 

 

1-2. Noise 함수 – 부드러운 랜덤 패턴

UV 좌표를 입력받아 부드럽게 변화하는 랜덤 값을 생성한다.

float Noise(float2 uv)
{
    float2 i = floor(uv);
    float2 f = frac(uv);

    // 4개의 꼭지점에서 난수값을 가져옴
    float a = Hash(i);
    float b = Hash(i + float2(1,0));
    float c = Hash(i + float2(0,1));
    float d = Hash(i + float2(1,1));
	
    // SmoothStep 보간 
    float2 u = f * f * (3.0 - 2.0 * f); 
	
    // 사각형 내부를 부드럽게 보간 
    float lerp_x0 = lerp(a,b,u.x);
    float lerp_x1 = lerp(c,d,u.x);
    return lerp(lerp_x0, lerp_x1,u.y);
}

 

픽셀 단위로 자연스럽게 연결된 랜덤 패턴 생성
  1. UV를 정수 그리드(i)와 그 안의 위치(f)로 분리
  2. 그리드 꼭짓점 4곳에서 Hash 난수값 가져오기 
  3. Smoothstep 보간으로 값의 급격한 변화를 제거
  4. Bilinear interpolation(양선형 보간법)으로 사각형 내부 값을 계산
    • bilinear interpolation : linear interpolation을 x축과 y축으로 두 번 적용하여 값을 유추하는 방법이다. 

 

 

1-3. FBM – 여러 Noise를 쌓아 자연스럽게

Noise를 여러 번 겹쳐서 자연계에서 보이는 복잡한 패턴을 생성한다. 

float FBM(float2 uv)
{
    float value = 0.0;
    float amplitude = 0.5;  // 초기 진폭
    float frequency = 1.0;  // 초기 주파수

    // 여러 옥타브(Noise 레이어)를 합성
    for(int i=0;i<4;i++)
    {
        value += amplitude * Noise(uv * frequency);
        frequency *= 2.0;  // 다음 레이어는 주파수를 2배로
        amplitude *= 0.5;  // 진폭은 반으로 줄여서 영향력 감소
    }
    return value;
}

 

  • 낮은 주파수 → 큰 연기 덩어리
  • 높은 주파수 → 작은 디테일과 잔난류
하나의 Noise만 사용하면 단조롭기 때문에, 서로 다른 크기의 패턴을 합성하여 연기처럼 보이도록 했다.

 

 

 

 

2. 연기 움직임 설계 (Flow + Domain Warping)

 

2-1. 기본 상승 흐름 (Advection)

연기는 기본적으로 위로 상승한다는 물리적 특성을 구현

float2 flowDir = float2(0.0, -1.0);
float2 advectedUV = uv + flowDir * time * 0.1;

 

시간에 따라 UV 자체를 위쪽으로 이동시켜, 연기 전체가 화면 위로 흐르는 느낌을 만듦

 

 

2-2. 큰 흐름 (Large Scale Warping)

연기 덩어리 단위의 느리고 큰 흔들림을 구현 

float2 largeWarp = float2(
    FBM(advectedUV * 0.6 + time * 0.05),
    FBM(advectedUV * 0.6 - time * 0.05)
);
  • 낮은 주파수
  • 느린 시간 변화
  • 연기 전체가 휘어지는 방향성 형성

 

2-3. 작은 난류 (Small Turbulence)

연기 표면의 미세한 요동과 디테일을 구현 

float2 smallWarp = float2(
    FBM(advectedUV * 2.0 + time * 0.2),
    FBM(advectedUV * 2.0 - time * 0.2)
);
  • 높은 주파수
  • 빠른 변화
  • 연기가 끓거나 퍼지는 느낌을 추가

 

2-4. Domain Warping 합성

큰 흐름 + 작은 난류를 함께 적용한 Domain Warping

float2 warp = largeWarp * 1.2 + smallWarp * 0.4;

// 왜곡된 좌표에 FBM을 다시 적용: 연기의 진하고 옅은 부분(밀도) 생성
float density = FBM(advectedUV + warp);
  • 연기가 일직선으로 움직이지 않음
  • 반복 패턴이 눈에 띄지 않음
  • 자연스러운 일렁임 형성

 

 

 

3. 높이에 따른 밀도 감쇠 (Height Fade)

연기가 위로 갈수록 얇아지고 사라지는 현상 구현

float fadeNoise = FBM(uv * 1.5);
float heightFade = smoothstep(1.0, 0.2, uv.y + fadeNoise * 0.1);

density *= heightFade; // 최종 밀도에 높이 감쇠 적용
  • uv.y를 이용해 화면 상단으로 갈수록 감쇠
  • Noise를 섞어 균일하게 사라지지 않도록 조정

 

 

4. 최종 색상 출력 

float4 smokeColor = float4(0.8, 0.8, 0.8, density);
return smokeColor;
  • RGB  :  연한 회색 → 연기 색상
  • Alpha  :  FBM 기반 밀도값 → 픽셀별 투명도

 

 

5. 전체 흐름 요약  

  역할 설명 
1 Hash 좌표 기반 난수 생성
2 Noise 부드러운 랜덤 패턴 형성
3 FBM 여러 크기의 패턴 합성
4 Advection 연기의 기본 상승 흐름
5 Domain Warping 큰 흐름 + 작은 난류로 자연스러운 일렁임 생성
6 Height Fade 위로 갈수록 연기가 옅어지는 효과
7 Density → Alpha 연기 투명도 결정

 

 

 

 

6. 전체 코드

자세한 구현 내용은 아래 깃허브에 올려져있다.

https://github.com/MoonSun-v/D3DProgramming/tree/main/D3DProgramming/02_1.SmokeRectangle

 

#include <Shared.fxh>

//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------


// [ Hash 함수 ]
//  0~1 사이의 난수 값 반환
//  - dot()으로 좌표를 하나의 값으로 압축
//  - sin()과 큰 상수를 곱해 난수처럼 보이게 변형
//  - frac()으로 소수 부분만 남겨 0~1 범위로 제한
float Hash(float2 p)
{
    return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}


// [ Noise 함수 ]
//  uv 좌표를 넣으면 부드러운 변화하는 랜덤 값을 반환
//  - UV를 정수 그리드(i)와 그 안의 위치(f)로 분리
//  - 그리드 꼭짓점 4곳에서 Hash 난수값 가져오기 
//  - Smoothstep 보간으로 값의 급격한 변화를 제거
//  - Bilinear interpolation(양선형 보간법)으로 사각형 내부 값을 계산
float Noise(float2 uv)
{
    float2 i = floor(uv);   // 현재 좌표의 정수 부분 (그리드 위치)
    float2 f = frac(uv);    // 소수 부분 (그리드 내 위치, 0~1)
    
    float a = Hash(i);                  // 왼쪽 아래
    float b = Hash(i + float2(1, 0));   // 오른쪽 아래
    float c = Hash(i + float2(0, 1));   // 왼쪽 위
    float d = Hash(i + float2(1, 1));   // 오른쪽 위
    
    float2 u = f * f * (3.0 - 2.0 * f); // Smoothstep 보간값 계산

    // bilinear interpolation (사각형 내부를 부드럽게 보간)
    // bilinear interpolation : linear interpolation을 x축과 y축으로 두 번 적용하여 값을 유추하는 방법
    float lerp_x0 = lerp(a, b, u.x);        // 아래쪽
    float lerp_x1 = lerp(c, d, u.x);        // 위쪽
    
    return lerp(lerp_x0, lerp_x1, u.y);     // 위아래 보간 -> 최종 값
}


// [ FBM (Fractional Brownian Motion) : 여러 레이어의 Noise 합성 ]
//  입력 좌표 uv에 대해 여러 주파수/진폭의 노이즈를 합쳐 자연스러운 패턴 생성
float FBM(float2 uv)
{
    float value = 0.0;      // 최종 fbm 값
    float amplitude = 0.5;  // 초기 진폭
    float frequency = 1.0;  // 초기 주파수

    // 여러 옥타브(Noise 레이어)를 합성
    for (int i = 0; i < 4; i++)
    {
        value += amplitude * Noise(uv * frequency); // 각 레이어 Noise 합치기
        frequency *= 2.0; // 다음 레이어는 주파수를 2배로 -> 더 세밀한 패턴
        amplitude *= 0.5; // 진폭은 반으로 줄여서 영향력 감소
    }
    return value;
}


float4 main(PS_INPUT input) : SV_TARGET
{ 
    
    float2 uv = input.uv; // 현재 픽셀의 UV 좌표 (0~1 범위) : 화면에서 이 픽셀이 어디 위치하는지를 나타냄
    

    //=====================================================================
    // 1. 기본 흐름 (전체가 위로 이동하는 큰 움직임)
    //=====================================================================
    // - advectedUV: 연기가 시간에 따라 이동한 위치
    
    float2 flowDir = float2(0.0, -1.0);             // 위쪽 : Y 방향 음수(-)
    float2 advectedUV = uv + flowDir * time * 0.1;  // time 에 따라 UV를 위쪽으로 이동

    

    //=====================================================================
    // 2. 큰 흐름 (연기 덩어리 단위의 느린 흔들림) , 주파수: 0.6 
    //=====================================================================
    // - largeWarp: 연기 전체가 좌우로 휘어지는 "큰 흐름" 역할
    
    float2 largeWarp = float2(
    FBM(advectedUV * 0.6 + time * 0.05), // X 방향 왜곡
    FBM(advectedUV * 0.6 - time * 0.05)  // Y 방향 왜곡
    );

    

    //=====================================================================
    // 3. 작은 난류 (연기 표면의 잔잔한 요동) , 주파수: 2.0
    //=====================================================================
    // - smallWarp: 연기 표면의 미세하게 끓는 듯한 느낌
    
    float2 smallWarp = float2(
    FBM(advectedUV * 2.0 + time * 0.2),
    FBM(advectedUV * 2.0 - time * 0.2)
    );

    

    //=====================================================================
    // 4. Domain Warping (큰 흐름 + 작은 난류 합성)
    //=====================================================================
    // - warp: UV 좌표를 얼마나 왜곡할지 결정하는 값
    
    // 큰 흐름은 강하게, 작은 난류는 약하게 섞기
    // -> 전체적인 방향성 유지하면서 자연스러운 디테일 추가 
    
    float2 warp = largeWarp * 1.2 + smallWarp * 0.4;

    

    //=====================================================================
    // 5. 왜곡된 UV로 연기 밀도 계산
    //=====================================================================
    // - density: 값이 클수록 연기가 진하고, 작을수록 연기가 옅음
    
    // 왜곡된 좌표에 FBM을 다시 적용: 연기의 진하고 옅은 부분(밀도) 생성
    float density = FBM(advectedUV + warp);



    //=====================================================================
    // 6. 위로 갈수록 연기가 사라지는 효과 (높이에 따른 밀도 감쇠)
    //=====================================================================

    // 약간의 노이즈를 섞어서 위쪽이 균일하게 사라지지 않도록 함
    float fadeNoise = FBM(uv * 1.5);

    // uv.y가 커질수록(위로 갈수록) 값이 줄어듦 -> smoothstep으로 부드러운 감쇠 곡선 생성
    float heightFade = smoothstep(1.0, 0.2, uv.y + fadeNoise * 0.1);

    // 최종 밀도에 높이 감쇠 적용
    density *= heightFade;

    
    
    //=====================================================================
    // 7. 최종 색상 출력
    //=====================================================================

    // RGB는 연기 색상 (연한 회색)
    // Alpha는 연기 밀도 -> 진할수록 불투명
    float4 smokeColor = float4(0.8, 0.8, 0.8, density);

    
    return smokeColor;
}

 

 

 

7. 구현 영상