🔧 OPENRNDR Post Processing

BamgasiJM·4일 전

OPENRNDR

목록 보기
15/17
post-thumbnail

이 문서는 “그려진 이미지를 어떻게 다루는가”에 초점을 맞췄습니다. 기본은 RenderTarget에 그리기 → Filter.apply → 화면 출력이고, 레이어·블렌드·다중 post가 필요해지면 orx-compositor, 전체 화면 체인만 필요하면 Post 확장, 픽셀 단위 제어가 필요하면 커스텀 Filter + GLSL로 확장하면 됩니다.

RenderTarget · Filter · orx-fx · Blur · Color · Blend · Distortion · Custom GLSL · Feedback · Ping-Pong · Compositor의 개념을 다룹니다.


공통 import

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.fx.blur.*
import org.openrndr.extra.fx.color.*
import org.openrndr.extra.fx.blend.*
import org.openrndr.extra.fx.distort.*
import org.openrndr.extra.compositor.*
import org.openrndr.math.Vector2
import kotlin.math.cos
import kotlin.math.sin
  • orx-fx, orx-compositor는 ORX 모듈입니다. 템플릿에서 해당 기능을 활성화한 뒤 IDE 자동 import를 사용하시기 바랍니다.

1. Post Processing의 개념

1.1 렌더링과 후처리의 차이

  • 렌더링(드로잉): drawer.circle, drawer.image, drawer.vertexBuffer 등으로 지오메트리·이미지를 그리는 단계입니다. 결과가 바로 화면(백버퍼)에 쓰이거나, 오프스크린 버퍼에 쓰입니다.
  • 후처리(Post Processing): 이미 그려진 이미지(ColorBuffer) 를 입력으로 받아, 픽셀 단위로 변형·합성하는 단계입니다. 블러, 색 보정, 왜곡, 블룸 등이 여기에 해당합니다.

즉, “무엇을 그릴지”가 렌더링이고, “그린 결과를 어떻게 보이게 할지”가 후처리입니다.

1.2 화면 출력 전 이미지 처리

전형적인 흐름은 다음과 같습니다.

장면 그리기 → (오프스크린 ColorBuffer) → Filter 적용 → (결과 ColorBuffer) → drawer.image로 화면 출력

화면(백버퍼)에 직접 그린 뒤에도 후처리를 할 수 있지만, OPENRNDR에서는 RenderTarget에 그린 뒤 Filter를 적용하는 패턴이 기본입니다.

1.3 RenderTarget과 Post Processing의 관계

  • RenderTarget은 “그릴 수 있는 오프스크린 버퍼”입니다. colorBuffer() 첨부가 있어야 이미지가 저장됩니다.
  • Filter는 ColorBuffer를 입력·출력으로 받습니다. 따라서:
    1. renderTarget { colorBuffer() }로 장면을 그림
    2. filter.apply(sourceColorBuffer, targetColorBuffer)로 후처리
    3. drawer.image(target)으로 화면에 표시

orx-compositor와 Post 확장은 이 버퍼 관리와 체인 구성을 자동화합니다.


2. RenderTarget 기반 후처리

2.1 Off-screen Rendering

val offscreen = renderTarget(width, height) {
    colorBuffer()
    depthBuffer()   // Shape/contour 또는 3D를 그릴 때 필요
}

drawer.isolatedWithTarget(offscreen) {
    drawer.clear(ColorRGBa.BLACK)
    drawer.fill = ColorRGBa.PINK
    drawer.stroke = null
    drawer.circle(width / 2.0, height / 2.0, 100.0)
}
  • isolatedWithTarget은 타깃을 바인딩하고, 블록이 끝나면 이전 타깃으로 복원합니다.
  • 크기가 창과 다르면 ortho(rt) 등으로 투영을 맞춥니다.

2.2 ColorBuffer

  • GPU 메모리에 있는 이미지입니다. colorBuffer(width, height) 또는 renderTarget의 colorBuffer(0)으로 얻습니다.
  • 포맷(ColorFormat), 타입(ColorType.UINT8, FLOAT16 등)을 지정할 수 있습니다.
  • Filter의 입·출력으로 쓰이며, drawer.image(cb)로 화면에 그릴 수 있습니다.

2.3 RenderTarget → 화면 출력 구조

공식 가이드 기본 패턴:

fun main() = application {
    configure { width = 770; height = 578 }
    program {
        val offscreen = renderTarget(width, height) {
            colorBuffer()
            depthBuffer()
        }
        val blur = BoxBlur()
        val blurred = colorBuffer(width, height)

        extend {
            drawer.isolatedWithTarget(offscreen) {
                clear(ColorRGBa.BLACK)
                fill = ColorRGBa.PINK
                stroke = null
                circle(
                    cos(seconds) * 100.0 + width / 2,
                    sin(seconds) * 100.0 + height / 2.0,
                    100.0 + 100.0 * cos(seconds * 2.0)
                )
            }
            blur.window = 30
            blur.apply(offscreen.colorBuffer(0), blurred)
            drawer.image(blurred)
        }
    }
}

2.4 여러 RenderTarget 연결하기

  • 한 버퍼의 출력을 다음 Filter의 입력으로 넘기면 체인이 됩니다.
  • 같은 버퍼를 입력·출력으로 쓰는 in-place 적용도 가능하지만, 이웃 픽셀을 읽는 Filter(블러 등)에서는 별도의 출력 버퍼를 쓰는 것이 안전합니다.
  • 알파 합성이 필요하면 ColorRGBa.TRANSPARENT로 clear한 뒤 여러 타깃을 순서대로 drawer.image로 올립니다.

3. OPENRNDR Filter 시스템

3.1 Filter의 기본 구조

  • Filter는 프래그먼트 셰이더 기반의 이미지 처리 단위입니다.
  • 입력 개수에 따라 Filter1to1, Filter2to1 등으로 나뉩니다.
  • apply(source, target) 또는 apply(arrayOf(src0, src1), target) 형태로 호출합니다.
  • orx-fx의 필터는 pre-multiplied alpha 입력을 가정합니다(OPENRNDR 기본).

3.2 Filter 적용 방법

val blur = BoxBlur()
blur.window = 30
blur.apply(sourceBuffer, targetBuffer)
  • 파라미터는 적용 전에 설정합니다.
  • 결과는 targetBuffer에 쓰이며, 이후 drawer.image(targetBuffer)로 표시합니다.

3.3 Filter Parameter

  • orx-fx 필터는 window, sigma, gain 등 프로퍼티로 파라미터를 노출합니다.
  • 커스텀 Filter에서는 var name: Double by parameters로 유니폼을 연결합니다(공식 Noise 예제).

3.4 Filter chain

여러 Filter를 순차 적용할 때 중간 버퍼가 필요합니다.

blur.apply(input, temp)
colorCorr.apply(temp, output)

Post 확장이나 orx-compositor의 post { }를 쓰면 중간 버퍼 할당·리사이즈를 자동화할 수 있습니다.

extend(Post()) {
    val blur = ApproximateGaussianBlur()
    val add = Add()
    post { input, output ->
        blur.window = 50
        blur.sigma = 50.0
        blur.apply(input, intermediate[0])
        add.apply(arrayOf(input, intermediate[0]), output)
    }
}

4. Blur

4.1 Box Blur

  • 단순 평균. 빠르고 품질은 낮습니다.
  • 대표 파라미터: window (커널 반경 관련).
val blur = BoxBlur()
blur.window = 30
blur.apply(src, dst)

4.2 Gaussian Blur

클래스특징
BoxBlur빠름, 품질 낮음
ApproximateGaussianBlur중간 속도·품질 (실무에서 자주 사용)
GaussianBlur정밀, 상대적으로 느림
HashBlur노이즈 기반, 예술적
ZoomBlur중심 방향 줌 블러
FrameBlur시간축 평균(트레일 성격)
val blur = ApproximateGaussianBlur()
blur.window = 25
blur.sigma = 10.0
blur.apply(src, dst)

4.3 Blur Radius (window / sigma)

  • window: 샘플 범위(픽셀 단위에 가까운 정수).
  • sigma: 가우시안 표준편차. 클수록 더 부드럽게 퍼집니다.
  • 애니메이션 시 sigma = cos(seconds) * 10.0 + 10.01처럼 시간에 묶을 수 있습니다.

4.4 Glow와 Bloom의 기본 원리

  • Bloom: 밝은 영역을 추출 → 블러 → 원본에 가산 합성.
  • orx-fx에 Bloom, MipBloom 등이 있습니다.
  • 수동 구성 예:
// 1) 밝은 부분만 남기는 처리(또는 원본 복사)
// 2) blur.apply(bright, bloomBuf)
// 3) Add().apply(arrayOf(original, bloomBuf), output)

Post 확장의 Add + ApproximateGaussianBlur 조합이 공식 데모에 자주 등장합니다.


5. Color Processing

5.1 orx-fx Color 계열 (개요)

  • ColorCorrection, ChromaticAberration, 톤맵(Uncharted2Tonemap) 등이 제공됩니다.
  • 밝기·대비·채도·색상은 ColorCorrection 계열 또는 colorMatrix로 조절하는 패턴이 일반적입니다.

5.2 Brightness / Contrast / Saturation / Hue / Gamma

  • Drawer 단계: drawer.drawStyle.colorMatrix에 행렬을 넣어 그리는 시점에 색을 바꿀 수 있습니다.
  • 후처리 단계: ColorCorrection 필터 파라미터로 조절(구체적인 프로퍼티 이름은 orx-fx 소스·데모 확인).

5.3 Color Matrix

drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.opacify(0.5))
// 또는 커스텀 행렬
  • 합성·마스킹 예제에서 이미지 불투명도를 바꿀 때 자주 쓰입니다.

6. Blend와 Compositing

6.1 Alpha Blending

  • OPENRNDR 기본은 pre-multiplied alpha입니다.
  • 투명 배경: drawer.clear(ColorRGBa.TRANSPARENT) 후 레이어를 쌓습니다.

6.2 BlendMode (Drawer)

  • drawer.drawStyle.blendMode = BlendMode.ADD 등.
  • 가이드에 나열된 모드: OVER, ADD, MULTIPLY, SCREEN, … 등.

6.3 Additive / Multiply / Screen (orx-fx blend)

  • Add(), Multiply(), Normal() 등은 두 입력을 받는 Filter2to1이며, compositor의 blend { }에 쓰입니다.
  • 모두 opacity를 보존하는 설계입니다.

6.4 여러 레이어 합성

공식 compositor 패턴:

val composite = compose {
    draw {
        drawer.clear(ColorRGBa.PINK)
        drawer.circle(width / 2.0, height / 2.0, 175.0)
    }
    layer {
        blend(Add()) { clip = true }
        draw {
            drawer.circle(width / 2.0, height / 2.0 + 100.0, 100.0)
        }
        post(ApproximateGaussianBlur()) {
            window = 25
            sigma = 10.0
        }
    }
}
extend {
    composite.draw(drawer)
}
  • blend는 “아래 레이어와 어떻게 합칠지”, post는 “이 레이어 그린 뒤 어떤 필터를 적용할지”입니다.

7. Distortion

7.1 UV 좌표 변형

  • 후처리 왜곡의 핵심은 샘플링 좌표(UV)를 움직이는 것입니다.
  • 프래그먼트 셰이더에서 texture(tex0, distortedUV) 형태로 구현합니다.

7.2 orx-fx Distortion 예

  • HorizontalWave, VerticalWave: 수평/수직 물결.
  • ChromaticAberration: RGB 채널 분리.
  • 그 외 Fisheye, Lenses, FluidDistort 등(README 목록 참고).
post(HorizontalWave()) {
    amplitude = cos(seconds * 3) * 0.1
    frequency = sin(seconds * 2) * 4
    segments = 10
    phase = seconds
}
post(VerticalWave()) { /* ... */ }
post(ApproximateGaussianBlur()) {
    sigma = cos(seconds * 2) * 5.0 + 5.01
    window = 25
}

적용 순서가 결과에 크게 영향을 줍니다(왜곡 후 블러 vs 블러 후 왜곡).

7.3 Noise Distortion / Chromatic Aberration

  • 노이즈로 UV를 밀면 유기적인 왜곡이 됩니다(커스텀 셰이더 또는 Perturb 계열).
  • ChromaticAberration은 채널별 오프셋으로 렌즈 수차 느낌을 냅니다.

8. Image Processing과 GLSL

8.1 Filter와 GLSL Shader의 관계

  • Filter는 전체 화면(또는 버퍼)을 덮는 쿼드를 그리고, 프래그먼트 셰이더로 각 픽셀 색을 계산합니다.
  • OPENRNDR Filter가 정점·투영·상태 설정을 담당하므로, 사용자는 주로 fragment 셰이더만 작성합니다.

8.2 Texture Sampling / Pixel 연산

표준 인터페이스(공식 Noise 필터):

in vec2 v_texCoord0;
uniform sampler2D tex0;
out vec4 o_color;
  • v_texCoord0: 0~1 UV.
  • texture(tex0, uv)로 입력 픽셀을 읽습니다.
  • 이웃 픽셀은 uv + vec2(1.0/width, 0.0)처럼 오프셋을 더해 샘플합니다.

9. Custom Post Processing Shader

9.1 GLSL Filter 작성 (공식 예제)

val noiseShader = """
#version 330
in vec2 v_texCoord0;
uniform sampler2D tex0;
out vec4 o_color;

uniform float gain;
uniform float time;

#define HASHSCALE 443.8975
vec2 hash22(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * HASHSCALE);
    p3 += dot(p3, p3.yzx + 19.19);
    return fract(vec2((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y));
}

void main() {
    float n = hash22(v_texCoord0 + vec2(time)).x;
    vec4 color = texture(tex0, v_texCoord0) + vec4(vec3(n), 0.0) * gain;
    o_color = color;
}
"""

class Noise : Filter(filterShaderFromCode(noiseShader, "noise-shader")) {
    var gain: Double by parameters
    var time: Double by parameters
    init {
        gain = 1.0
        time = 0.0
    }
}

사용:

noise.time = seconds
noise.gain = 1.0
noise.apply(offscreen.colorBuffer(0), offscreen.colorBuffer(0))
  • 이웃을 읽지 않는 필터만 in-place(src == dst)가 안전합니다. 블러처럼 주변을 읽으면 별도 출력 버퍼를 쓰세요.

9.2 Resolution / UV / Texture / Time / Mouse

  • 해상도: uniform vec2 textureSize0 또는 textureSize(tex0, 0) (버전에 따라 제공 방식이 다를 수 있음).
  • UV: v_texCoord0.
  • Time / Mouse: Kotlin에서 parameters로 넘긴 뒤 uniform float time 등으로 사용.

10. Feedback Rendering

10.1 이전 프레임을 입력으로 사용하기

  • 매 프레임 “현재 장면 + 이전 결과의 일부”를 합치면 트레일·잔상이 생깁니다.
  • 한 장의 ColorBuffer를 유지하고, 매 프레임 약간 어둡게 덮은 뒤 새 그림을 올리는 방식이 대표적입니다.

10.2 Trail / Fade / Feedback Loop

개념적 패턴:

// persistent: 누적 버퍼
drawer.isolatedWithTarget(persistent) {
    // 1) 전체를 약간 투명한 검정으로 덮어 fade
    drawer.fill = ColorRGBa.BLACK.opacify(0.05)
    drawer.stroke = null
    drawer.rectangle(drawer.bounds)
    // 2) 새 내용 그리기
    drawer.fill = ColorRGBa.PINK
    drawer.circle(...)
}
drawer.image(persistent.colorBuffer(0))
  • orx-no-clear 확장으로 “화면을 지우지 않고” 그리는 방법도 있습니다(캔버스형 트레일).
  • FrameBlur는 시간축 블러에 가깝습니다.

10.3 RenderTarget을 이용한 누적

  • 누적 버퍼와 “이번 프레임 전용” 버퍼를 나누면, 누적만 fade하고 새 레이어는 선명하게 유지할 수 있습니다.
  • 피드백이 발산하지 않도록 매 프레임 감쇠(알파 < 1) 를 넣는 것이 중요합니다.

11. Ping-Pong Rendering

11.1 두 RenderTarget의 교대 사용

이웃 픽셀을 읽거나, 시뮬레이션을 반복할 때 읽기 버퍼와 쓰기 버퍼를 분리합니다.

A (read) → Filter → B (write)
swap: B가 다음 read, A가 다음 write

11.2 Read/Write framebuffer 분리

var read = colorBuffer(w, h)
var write = colorBuffer(w, h)

// 매 프레임 또는 반복 루프
filter.apply(read, write)
val tmp = read; read = write; write = tmp
  • 같은 텍스처를 동시에 읽고 쓰면 정의되지 않은 결과가 나옵니다. Ping-pong이 이를 막습니다.

11.3 반복적인 GPU simulation / Feedback 안정성

  • 유체·반응-확산·파티클 필드 등은 여러 패스 × ping-pong으로 구현합니다.
  • 수치 발산을 막기 위해 감쇠, 클램핑, 적절한 타임스텝을 사용합니다.
  • 버퍼 타입을 FLOAT16/FLOAT32로 올리면 정밀도가 좋아질 수 있습니다.

12. Post Processing Compositor

12.1 orx-compositor 개요

  • compose { } DSL로 레이어 트리, blend, post 체인을 선언합니다.
  • 버퍼 할당·합성 순서를 프레임워크가 관리합니다.
  • orx-fx와 함께 쓰는 것이 일반적입니다.

12.2 여러 효과의 순차적 적용

layer {
    draw { /* ... */ }
    post(HorizontalWave()) { /* params */ }
    post(VerticalWave()) { /* params */ }
    post(ApproximateGaussianBlur()) { /* params */ }
}
  • post 호출 순서 = 적용 순서입니다.
  • 왜곡 → 블러와 블러 → 왜곡은 전혀 다른 결과가 됩니다.

12.3 Layer Compositing / 적용 순서가 결과에 미치는 영향

구성효과
blend(Add()) { clip = true }가산 + 아래 레이어로 클리핑
blend(Normal()) { clip = true }일반 알파 + 마스크처럼 사용
중첩 layer { layer { } }마스크·그림자를 분리해 합성
post 다중 호출필터 체인

마스킹 예(텍스트가 있는 곳에만 이미지):

  • 안쪽 레이어에 텍스트
  • 그 위 레이어에 이미지 + blend(Normal()) { clip = true }

드롭 섀도:

  • 텍스트 레이어에 post(DropShadow()) { window = 10; gain = 1.0; ... }

12.4 Post 확장 vs Compositor

방식적합한 경우
수동 RenderTarget + Filter단순 1~2단 후처리, 버퍼를 직접 제어할 때
Post() 확장전체 화면 후처리, 중간 버퍼 자동 관리
orx-compositor다중 레이어, 블렌드, 레이어별 post, 마스크

13. 검증 메모

항목내용
코드 실행 여부공식 Filters / Compositor 예제 기반. Ping-pong·커스텀 체인 조합은 0.5.0 직접 실행 미검증
ORX importBoxBlur, ApproximateGaussianBlur, Add, compose 등 IDE 자동 import
in-place apply이웃 샘플 Filter는 src ≠ dst 권장
premultiplied alphaorx-fx는 premultiplied 입력 가정
content scale고DPI에서 블러 window/sigma 체감이 달라질 수 있음 (0.5.0에서 일부 수정)
파라미터 이름ColorCorrection 등 세부 프로퍼티는 orx-fx 소스·데모 확인

14. 자주 쓰는 패턴 요약

하고 싶은 일사용할 기능
장면 블러 후 표시RT에 그리기 → BoxBlur/ApproximateGaussianBlur.apply → drawer.image
블룸블러 + Add (또는 Bloom)
전체 화면 후처리 체인extend(Post()) { post { ... } }
레이어별 효과·마스크orx-compositor의 layer / blend / post
물결·수차HorizontalWave, VerticalWave, ChromaticAberration
커스텀 픽셀 효과Filter 상속 + GLSL fragment
잔상·트레일누적 RT + 매 프레임 fade, 또는 no-clear
안정적 피드백/시뮬Ping-pong 두 버퍼

15. 성능 관점의 정리

  1. 버퍼·Filter 생성: program { }에서 한 번만 만들고 재사용합니다. 매 프레임 colorBuffer() 생성은 피합니다.
  2. 해상도: 후처리 해상도를 화면의 1/2로 낮추면 블러·왜곡 비용이 크게 줄어듭니다.
  3. 체인 길이: post를 많이 쌓을수록 패스가 늘어납니다. 필요한 최소 단수로 유지합니다.
  4. Gaussian vs Approximate: 실시간에서는 ApproximateGaussianBlur가 실용적입니다.
  5. in-place: 가능하면 별도 출력을 쓰고, ping-pong으로 읽기/쓰기를 분리합니다.
  6. compositor: 레이어·post가 많으면 내부적으로 여러 버퍼를 씁니다. 프로파일하며 줄입니다.

16. 문서에서 다루지 않은 항목

  • orx-fx 전체 필터 목록의 파라미터 상세(버전별로 소스 확인)
  • Compute shader 기반 이미지 처리
  • 다중 렌더 타깃(MRT) 고급 구성
  • orx-temporal-blur (비실시간 지향)
  • 비디오 프레임 단위 후처리 파이프라인 상세

17. 참고 자료

공식 가이드

저장소

릴리즈

  • OPENRNDR 0.5.0 / ORX 0.5.0 릴리즈 노트

profile
Coding Art with Blender / Three.js / p5.js / GLSL Shader / Processing / nannou

0개의 댓글