๐Ÿ”ง OPENRNDR ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ

BamgasiJMยท4์ผ ์ „

OPENRNDR

๋ชฉ๋ก ๋ณด๊ธฐ
13/17
post-thumbnail

๊ธฐ๋ณธ ๋„ํ˜• ํ•จ์ˆ˜ ยท ์ƒ‰ ์ง€์ • ยท ๋„ํ˜•์„ ๋งˆ์Šคํฌ๋กœ ์“ฐ๋Š” ๋ฐฉ๋ฒ• ยท ๋„ํ˜• ์•ˆ์— GLSL ์…ฐ์ด๋” ๋„ฃ๊ธฐ๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

0. ๋ฌธ์„œ ๋ฒ”์œ„์™€ ๊ธฐ์ค€

0.1 ๊ธฐ์ค€ ๋ฒ„์ „

  • ๋ณธ ๊ฐ€์ด๋“œ์˜ ๊ธฐ์ค€ ๋ฒ„์ „์€ OPENRNDR 0.5.0(2026-09-16 ๋ฆด๋ฆฌ์ฆˆ)๊ณผ ORX 0.5.0์ž…๋‹ˆ๋‹ค.
  • ๋„ํ˜• ์ž‘์—…์— ๊ฐ„์ ‘์ ์œผ๋กœ ์˜ํ–ฅ์„ ์ฃผ๋Š” 0.5.0 ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๋ณ€๊ฒฝ ์‚ฌํ•ญ๋„ํ˜• ์ž‘์—…๊ณผ์˜ ๊ด€๊ณ„
SDL3 ๊ธฐ๋ฐ˜ ๋ฐฑ์—”๋“œ ์ถ”๊ฐ€, GLFW๋Š” deprecated์ฐฝ ์ƒ์„ฑยท์ž…๋ ฅ ๊ณ„์ธต์˜ ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ธฐ API์—๋Š” ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.
macOS์˜ Angle ๊ธฐ๋ฐ˜ GLES ๋ฐฑ์—”๋“œ์—์„œ shape/contour fill ๋ Œ๋”๋ง ์ˆ˜์ •macOS์—์„œ ์ฑ„์›Œ์ง„ shape๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ๋ณด์ด๋˜ ๋ฌธ์ œ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
Segment2D/3D์— ์ด์ฐจ(quadratic) ๊ทผ์‚ฌ ์ถ”๊ฐ€openrndr-math ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค.
Kotlin 2.4.20, Gradle 9.7.1, LWJGL 3.4.3๋นŒ๋“œ ํ™˜๊ฒฝ ์š”๊ฑด์ž…๋‹ˆ๋‹ค.
openrndr-module-catalog, openrndr-dependency-catalog ์ถ”๊ฐ€์˜์กด์„ฑ ์„ ์–ธ ๋ฐฉ์‹์ด ๋‹จ์ˆœํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

0.2 ๊ณตํ†ต import

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.shape.*
import kotlin.math.cos
import kotlin.math.sin
  • ORX ๋ชจ๋“ˆ(orx-compositor, orx-fx, orx-shade-styles, orx-shapes)์˜ ํด๋ž˜์Šค๋Š” ํŒจํ‚ค์ง€๊ฐ€ ๋ชจ๋“ˆ๋ณ„๋กœ ๋‚˜๋‰˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณธ๋ฌธ ์ฝ”๋“œ์—๋Š” ORX import๋ฅผ ์ ์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ IDE์˜ ์ž๋™ import๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

1. ์ค€๋น„

1.1 ํ”„๋กœ์ ํŠธ ์„ค์ •

  • openrndr-template์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ gradle/libs.versions.toml์—์„œ ๋ฒ„์ „์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
openrndr = "0.5.0"
orx = "0.5.0"
  • ํ…œํ”Œ๋ฆฟ์˜ ๊ธฐ๋ณธ orx ๋ฒˆ๋“ค์—๋Š” orx-compositor, orx-shade-styles ๋“ฑ์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ณต์‹ ๊ฐ€์ด๋“œ์— ๋ช…์‹œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฒ„์ „ ๋ณ€๊ฒฝ ํ›„์—๋Š” Gradle ์„ค์ •์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค.

1.2 ๊ธฐ๋ณธ ๊ณจ๊ฒฉ

fun main() = application {
    configure {
        width = 800
        height = 600
    }
    program {
        extend {
            drawer.clear(ColorRGBa.PINK)
            // ๊ทธ๋ฆฌ๊ธฐ ์ฝ”๋“œ
        }
    }
}

1.3 OPENRNDR ์ขŒํ‘œ๊ณ„์™€ ์ž๋ฃŒํ˜• ๊ทœ์น™

  • ์›์ ์€ ์ฐฝ์˜ ์™ผ์ชฝ ์œ„์ด๊ณ , x๋Š” ์˜ค๋ฅธ์ชฝ, y๋Š” ์•„๋ž˜์ชฝ์œผ๋กœ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • ์ขŒํ‘œ์™€ ํฌ๊ธฐ๋Š” ๋Œ€๋ถ€๋ถ„ Double์ž…๋‹ˆ๋‹ค. width, height๋Š” Int์ด๋ฏ€๋กœ width / 2.0, height * 1.0์ฒ˜๋Ÿผ Double๋กœ ๋ณ€ํ™˜ํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ๋งค ํ”„๋ ˆ์ž„ ๋ฐ˜๋ณตํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฐ์ฒด(ShadeStyle, ShapeContour, Shape, RenderTarget)๋Š” extend ๋ฐ”๊นฅ์˜ program { } ์•ˆ์—์„œ ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์›์น™์ž…๋‹ˆ๋‹ค.

2. ๊ธฐ๋ณธ ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ

2.1 Drawer ๋„ํ˜• ํ•จ์ˆ˜ ๊ฐœ์š”

ํ•จ์ˆ˜์ž…๋ ฅ์ ์šฉ๋˜๋Š” ์Šคํƒ€์ผ
circle(x, y, radius) / circle(Vector2, radius) / circle(Circle)์ค‘์‹ฌ ๊ธฐ์ค€fill, stroke, strokeWeight
circles(List<Circle>) / circles(List<Vector2>, radius)๋ฐฐ์น˜ ๊ทธ๋ฆฌ๊ธฐ๋™์ผ
rectangle(x, y, width, height) / rectangle(Rectangle)์™ผ์ชฝ ์œ„ ๊ธฐ์ค€fill, stroke, strokeWeight
rectangles { }๋ฐฐ์น˜ ๋นŒ๋”๋™์ผ
lineSegment(x0, y0, x1, y1)์„ ๋ถ„stroke, strokeWeight, lineCap
lineStrip(List<Vector2>)๊บพ์€์„ stroke, strokeWeight, lineCap, lineJoin
segment(Segment2D) / segments(List)๋ฒ ์ง€์— ์กฐ๊ฐstroke
contour(ShapeContour) / contours(List)์œค๊ณฝ์„ ๋‹ซํžŒ contour๋Š” fill ์‚ฌ์šฉ, stroke ์‚ฌ์šฉ
shape(Shape) / shapes(List)๊ตฌ๋ฉ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š” ๋„ํ˜•fill, stroke

2.2 ์›

  • ์›์€ (x, y)๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ทธ๋ ค์ง‘๋‹ˆ๋‹ค. ์ฑ„์›€์€ drawer.fill, ํ…Œ๋‘๋ฆฌ๋Š” drawer.stroke์™€ drawer.strokeWeight๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.
  • fill ๋˜๋Š” stroke์— null์„ ์ง€์ •ํ•˜๋ฉด ํ•ด๋‹น ๋ถ€๋ถ„์„ ๊ทธ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
fun main() = application {
    configure { height = 300 }
    program {
        extend {
            drawer.clear(ColorRGBa.PINK)

            // ํฐ ์ฑ„์›€ + ๊ฒ€์€ ํ…Œ๋‘๋ฆฌ
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = ColorRGBa.BLACK
            drawer.strokeWeight = 1.0
            drawer.circle(width / 6.0, height / 2.0, width / 8.0)

            // ์ฑ„์›€ ์—†์Œ, ํ…Œ๋‘๋ฆฌ๋งŒ
            drawer.fill = null
            drawer.stroke = ColorRGBa.BLACK
            drawer.circle(width / 6.0 + width / 3.0, height / 2.0, width / 8.0)

            // ์ฑ„์›€๋งŒ, ํ…Œ๋‘๋ฆฌ ์—†์Œ
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = null
            drawer.circle(width / 6.0 + 2 * width / 3.0, height / 2.0, width / 8.0)
        }
    }
}
  • Vector2๋‚˜ Circle ๊ฐ’์„ ์ด๋ฏธ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด ์˜ค๋ฒ„๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•ฉ๋‹ˆ๋‹ค.
drawer.circle(mouse.position, 50.0)          // Vector2 + ๋ฐ˜์ง€๋ฆ„
drawer.circle(Circle(200.0, 200.0, 40.0))    // Circle ๊ฐ์ฒด

2.3 ์‚ฌ๊ฐํ˜•

  • rectangle์€ ์™ผ์ชฝ ์œ„ ๋ชจ์„œ๋ฆฌ ์ขŒํ‘œ์™€ ๋„ˆ๋น„ยท๋†’์ด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ค‘์‹ฌ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฆฌ๋ ค๋ฉด Rectangle.fromCenter๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
extend {
    drawer.clear(ColorRGBa.PINK)
    drawer.fill = ColorRGBa.WHITE
    drawer.stroke = ColorRGBa.BLACK

    // ์™ผ์ชฝ ์œ„ ๊ธฐ์ค€
    drawer.rectangle(50.0, 50.0, 200.0, 120.0)

    // ์ค‘์‹ฌ ๊ธฐ์ค€: Rectangle ๊ฐ์ฒด๋ฅผ ๋งŒ๋“  ๋’ค ์ „๋‹ฌ
    val r = Rectangle.fromCenter(Vector2(width / 2.0, height / 2.0), 200.0, 120.0)
    drawer.rectangle(r)
}

2.4 ์„ ๋ถ„๊ณผ ๊บพ์€์„ 

  • ์„ ์€ stroke์™€ strokeWeight๋กœ ๊ทธ๋ ค์ง€๋ฉฐ fill์€ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์„  ๋ ๋ชจ์–‘์€ lineCap(BUTT, ROUND, SQUARE), ๊บพ์ด๋Š” ๋ถ€๋ถ„์€ lineJoin์œผ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
extend {
    drawer.clear(ColorRGBa.PINK)
    drawer.stroke = ColorRGBa.BLACK
    drawer.strokeWeight = 5.0

    drawer.lineCap = LineCap.ROUND
    drawer.lineSegment(10.0, height / 2.0 - 20.0, width - 10.0, height / 2.0 - 20.0)

    drawer.lineCap = LineCap.BUTT
    drawer.lineSegment(10.0, height / 2.0, width - 10.0, height / 2.0)

    drawer.lineCap = LineCap.SQUARE
    drawer.lineSegment(10.0, height / 2.0 + 20.0, width - 10.0, height / 2.0 + 20.0)

    // ๊บพ์€์„ 
    drawer.lineJoin = LineJoin.ROUND
    drawer.lineStrip(
        listOf(
            Vector2(10.0, height - 10.0),
            Vector2(width / 2.0, 10.0),
            Vector2(width - 10.0, height - 10.0)
        )
    )
}

2.5 ๋ณ€ํ™˜(translate / rotate / scale)๊ณผ ๋„ํ˜•

  • ๋ณ€ํ™˜์€ ์ขŒํ‘œ๊ณ„์˜ ์›์ ์„ ๊ธฐ์ค€์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋„ํ˜•์„ ์ œ์ž๋ฆฌ์—์„œ ํšŒ์ „์‹œํ‚ค๋ ค๋ฉด ๋„ํ˜•์„ ์›์  ์ฃผ์œ„์— ๊ทธ๋ฆฌ๊ณ , ๋ณ€ํ™˜์œผ๋กœ ํ™”๋ฉด ์œ„์น˜๋กœ ์˜ฎ๊น๋‹ˆ๋‹ค.
  • ๋ณ€ํ™˜ ์ฝ”๋“œ๋Š” ์•„๋ž˜์—์„œ ์œ„๋กœ ์ฝ์Šต๋‹ˆ๋‹ค. translate๋ฅผ ๋จผ์ € ์“ฐ๊ณ  rotate๋ฅผ ๋‚˜์ค‘์— ์“ฐ๋ฉด ์‹ค์ œ๋กœ๋Š” ํšŒ์ „์ด ๋จผ์ € ์ ์šฉ๋œ ๋’ค ์ด๋™๋ฉ๋‹ˆ๋‹ค.
  • isolated { }๋Š” ์Šคํƒ€์ผ๊ณผ ๋ณ€ํ™˜์„ ์ €์žฅํ–ˆ๋‹ค๊ฐ€ ๋ธ”๋ก์ด ๋๋‚˜๋ฉด ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค.
extend {
    drawer.clear(ColorRGBa.PINK)
    drawer.stroke = null

    for (i in 0 until 5) {
        drawer.isolated {
            translate(120.0 + i * 140.0, height / 2.0)
            rotate(seconds * 30.0 + i * 15.0)
            scale(1.0 + i * 0.15)
            fill = ColorRGBa.WHITE.shade(1.0 - i * 0.15)
            rectangle(-40.0, -40.0, 80.0, 80.0)     // ์›์  ๊ธฐ์ค€์œผ๋กœ ๋ฐฐ์น˜
        }
    }
}

2.6 ์ˆ˜ํ•™ ๊ฐ์ฒด์™€ Contour ยท Shape

OPENRNDR์˜ ๋„ํ˜• ํ‘œํ˜„์€ ์„ธ ๋‹จ๊ณ„๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๋‹จ๊ณ„ํด๋ž˜์Šค์„ค๋ช…
1Segment2D์‹œ์ž‘์ , ๋์ , ์ œ์–ด์  0~2๊ฐœ๋กœ ์ด๋ฃจ์–ด์ง„ ๋ฒ ์ง€์— ์กฐ๊ฐ(์ง์„ ยท2์ฐจยท3์ฐจ)
2ShapeContour๋๊ณผ ์‹œ์ž‘์ด ์ด์–ด์ง„ Segment ๋ชจ์Œ. ๋‹ซํžŒ ๊ฒƒ(O)๊ณผ ์—ด๋ฆฐ ๊ฒƒ(S)์ด ์žˆ์Œ
3ShapeShapeContour์˜ ๋ชจ์Œ. ๊ตฌ๋ฉ์ด ์žˆ๋Š” ๋„ํ˜•์„ ํ‘œํ˜„ ๊ฐ€๋Šฅ

Circle, Rectangle, LineSegment ๊ฐ™์€ ์ˆ˜ํ•™ ๊ฐ์ฒด๋Š” ํ™”๋ฉด์— ๊ทธ๋ฆฌ์ง€ ์•Š์•„๋„ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, .contour ๋˜๋Š” .shape๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

val c = Circle(200.0, 200.0, 50.0).contour   // ShapeContour
val s = Circle(200.0, 200.0, 50.0).shape     // Shape

2.6.1 ContourBuilder

  • SVG์™€ ์œ ์‚ฌํ•œ ๋ช…๋ น์–ด(moveTo, lineTo, curveTo, arcTo, close)๋กœ contour๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • cursor๋Š” ํ˜„์žฌ ์œ„์น˜, anchor๋Š” ํ˜„์žฌ ์„œ๋ธŒํŒจ์Šค์˜ ์‹œ์ž‘์ ์ž…๋‹ˆ๋‹ค.
fun main() = application {
    program {
        // ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋ฐ”๋€Œ์ง€ ์•Š์œผ๋ฏ€๋กœ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑ
        val triangle = contour {
            moveTo(Vector2(width / 2.0 - 120.0, height / 2.0 - 120.0))
            lineTo(cursor + Vector2(240.0, 0.0))
            lineTo(cursor + Vector2(0.0, 240.0))
            lineTo(anchor)
            close()
        }
        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.fill = ColorRGBa.PINK
            drawer.stroke = null
            drawer.contour(triangle)
        }
    }
}

2.6.2 ์  ๋ชฉ๋ก์—์„œ Contour ๋งŒ๋“ค๊ธฐ (์ •๋‹ค๊ฐํ˜•, ๋ณ„)

ShapeContour.fromPoints(points, closed)๋Š” ์ ๋“ค์„ ์ง์„ ์œผ๋กœ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ •๋‹ค๊ฐํ˜•๊ณผ ๋ณ„์€ ์ด ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

fun regularPolygon(center: Vector2, radius: Double, sides: Int, rotationDeg: Double = -90.0): ShapeContour {
    val points = List(sides) {
        val a = Math.toRadians(rotationDeg) + it * 2.0 * Math.PI / sides
        center + Vector2(cos(a), sin(a)) * radius
    }
    return ShapeContour.fromPoints(points, closed = true)
}

fun star(center: Vector2, outer: Double, inner: Double, tips: Int, rotationDeg: Double = -90.0): ShapeContour {
    val points = List(tips * 2) {
        val r = if (it % 2 == 0) outer else inner
        val a = Math.toRadians(rotationDeg) + it * Math.PI / tips
        center + Vector2(cos(a), sin(a)) * r
    }
    return ShapeContour.fromPoints(points, closed = true)
}
fun main() = application {
    configure { width = 800; height = 400 }
    program {
        val hexagon = regularPolygon(Vector2(200.0, 200.0), 120.0, 6)
        val starContour = star(Vector2(600.0, 200.0), 140.0, 60.0, 5)
        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.fill = ColorRGBa.PINK
            drawer.stroke = ColorRGBa.BLACK
            drawer.strokeWeight = 3.0
            drawer.contour(hexagon)
            drawer.contour(starContour)
        }
    }
}
  • ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„ ์ด ํ•„์š”ํ•˜๋ฉด orx-shapes์˜ hobbyCurve(points, closed)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต์‹ ๊ฐ€์ด๋“œ๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ShapeContour.fromPoints์˜ ๊ณก์„  ๋ฒ„์ „์œผ๋กœ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

2.6.3 Segment์—์„œ Contour ๋งŒ๋“ค๊ธฐ

๊ฐ Segment๋Š” ์ด์ „ ์กฐ๊ฐ์ด ๋๋‚˜๋Š” ์ง€์ ์—์„œ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

val segments = listOf(
    Segment2D(Vector2(10.0, 100.0), Vector2(200.0, 80.0)),                                       // ์ง์„ 
    Segment2D(Vector2(200.0, 80.0), Vector2(250.0, 280.0), Vector2(400.0, 80.0)),                // 2์ฐจ
    Segment2D(Vector2(400.0, 80.0), Vector2(450.0, 180.0), Vector2(500.0, 0.0), Vector2(630.0, 80.0)) // 3์ฐจ
)
val open = ShapeContour.fromSegments(segments, closed = false)

2.6.4 Contour ์†์„ฑ๊ณผ ๋ณ€ํ˜•

val pos = contour.position(0.1)                 // ์‹œ์ž‘ ๊ทผ์ฒ˜์˜ ์ 
val normal = contour.normal(0.9)                // ๋ ๊ทผ์ฒ˜์˜ ๋ฒ•์„ 
val bounds = contour.bounds                     // Rectangle
val length = contour.length                     // ๊ธธ์ด
val points = contour.equidistantPositions(20)   // ๋“ฑ๊ฐ„๊ฒฉ ์  20๊ฐœ
val part = contour.sub(0.0, 0.5)                // ์•ž ์ ˆ๋ฐ˜๋งŒ ์ž˜๋ผ๋‚ธ contour
val grown = contour.offset(10.0)                // ๋ฐ”๊นฅ์œผ๋กœ ์˜คํ”„์…‹
  • position(ut)์˜ ut๋Š” ๊ธธ์ด์— ๋น„๋ก€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋“ฑ์† ์ด๋™์ด ํ•„์š”ํ•˜๋ฉด orx-shapes์˜ rectified()๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

2.6.5 Shape์™€ ๊ตฌ๋ฉ

  • shape { contour { ... } contour { ... } } ๋นŒ๋”๋กœ ์—ฌ๋Ÿฌ contour๋ฅผ ๋ฌถ์Šต๋‹ˆ๋‹ค. ๊ณต์‹ ์˜ˆ์ œ์—์„œ๋Š” ๋ฐ”๊นฅ ์œค๊ณฝ๊ณผ ์•ˆ์ชฝ ์œค๊ณฝ์„ ํ•จ๊ป˜ ๋„ฃ์–ด ๊ตฌ๋ฉ์ด ์žˆ๋Š” ๋„ํ˜•์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
val s = shape {
    contour {
        moveTo(Vector2(width / 2.0 - 120.0, height / 2.0 - 120.0))
        lineTo(cursor + Vector2(240.0, 0.0))
        lineTo(cursor + Vector2(0.0, 240.0))
        lineTo(anchor)
        close()
    }
    contour {
        moveTo(Vector2(width / 2.0 - 80.0, height / 2.0 - 100.0))
        lineTo(cursor + Vector2(190.0, 0.0))
        lineTo(cursor + Vector2(0.0, 190.0))
        lineTo(anchor)
        close()
    }
}
drawer.shape(s)

2.6.6 Boolean ์—ฐ์‚ฐ (compound)

  • compound { } ๋นŒ๋”๋กœ ํ•ฉ์ง‘ํ•ฉ(union), ์ฐจ์ง‘ํ•ฉ(difference), ๊ต์ง‘ํ•ฉ(intersection)์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋Š” List<Shape>์ด๋ฉฐ drawer.shapes(...)๋กœ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 800; height = 400 }
    program {
        val ring = compound {
            difference {
                shape(Circle(200.0, 200.0, 140.0).shape)
                shape(Circle(200.0, 200.0, 90.0).shape)
            }
        }
        val lens = compound {
            intersection {
                shape(Circle(560.0, 160.0, 120.0).shape)
                shape(Circle(560.0, 240.0, 120.0).shape)
            }
        }
        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.fill = ColorRGBa.PINK
            drawer.stroke = ColorRGBa.PINK.shade(0.7)
            drawer.shapes(ring)
            drawer.shapes(lens)
        }
    }
}
  • compound๋Š” ํŠธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ์ง€์›ํ•˜๋ฏ€๋กœ union { intersection { ... } intersection { ... } }์ฒ˜๋Ÿผ ์ค‘์ฒฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ณ„์‚ฐ ๋น„์šฉ์ด ์žˆ์œผ๋ฏ€๋กœ ์ž…๋ ฅ์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š”๋‹ค๋ฉด extend ๋ฐ”๊นฅ์—์„œ ํ•œ ๋ฒˆ๋งŒ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

2.7 ๋ฐฐ์น˜(batch) ๊ทธ๋ฆฌ๊ธฐ

  • ๊ฐ™์€ ์ข…๋ฅ˜์˜ ๋„ํ˜•์„ ์ˆ˜์ฒœ ๊ฐœ ๊ทธ๋ฆด ๋•Œ๋Š” drawer.circle()์„ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค drawer.circles(list)๊ฐ€ ํ›จ์”ฌ ๋น ๋ฆ…๋‹ˆ๋‹ค.
fun main() = application {
    program {
        extend {
            drawer.clear(ColorRGBa.PINK)
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = ColorRGBa.BLACK
            drawer.strokeWeight = 1.0
            val circles = List(5000) {
                Circle(Math.random() * width, Math.random() * height, Math.random() * 10.0 + 5.0)
            }
            drawer.circles(circles)
        }
    }
}
  • ๊ฐœ๋ณ„ ์ƒ‰์ด ํ•„์š”ํ•˜๋ฉด ๋นŒ๋”๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ •์  ๋ฐฐ์น˜๋Š” drawer.rectangleBatch { }๋กœ ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
extend {
    drawer.clear(ColorRGBa.GRAY)
    drawer.rectangles {
        repeat(100) {
            fill = ColorRGBa.PINK.opacify(0.2)
            stroke = null
            rectangle(Rectangle((it * 37.0) % width, 0.0, 30.0, 5.0 + it * 3.0))
        }
    }
}

2.8 ์ฃผ์˜ ์‚ฌํ•ญ

  • ShapeContour์™€ Shape๋ฅผ RenderTarget์— ๊ทธ๋ฆด ๋•Œ๋Š” ์ปฌ๋Ÿฌ ๋ฒ„ํผ์™€ ํ•จ๊ป˜ ๊นŠ์ด(์Šคํ…์‹ค) ๋ฒ„ํผ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์—†์œผ๋ฉด drawing ... requires a render target with a stencil attachment ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
val rt = renderTarget(width, height) {
    colorBuffer()
    depthBuffer()   // contour/shape๋ฅผ ๊ทธ๋ฆด ๋•Œ ํ•„์š”
}
  • ๋ Œ๋” ํƒ€๊นƒ ํฌ๊ธฐ๊ฐ€ ์ฐฝ๊ณผ ๋‹ค๋ฅด๋ฉด ortho(rt)๋กœ ํˆฌ์˜ ํ–‰๋ ฌ์„ ๋งž์ถฐ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

3. ์ƒ‰ ์ง€์ •

3.1 ColorRGBa ๊ธฐ๋ณธ

  • OPENRNDR์˜ ๊ธฐ๋ณธ ์ƒ‰ ํด๋ž˜์Šค๋Š” ColorRGBa์ด๋ฉฐ ์ฑ„๋„ ๊ฐ’์€ 0.0~1.0์ž…๋‹ˆ๋‹ค.
  • ๋ฏธ๋ฆฌ ์ •์˜๋œ ์ƒ์ˆ˜: BLACK, WHITE, RED, GREEN, BLUE, YELLOW, GRAY, PINK. ์ด ๋ฐ–์— ORANGE, PURPLE, TRANSPARENT๋„ ๊ณต์‹ ์˜ˆ์ œ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
// ์ƒ์„ฑ์ž: R, G, B, (A)
val red = ColorRGBa(1.0, 0.0, 0.0)
val halfBlue = ColorRGBa(0.0, 0.0, 1.0, 0.5)

// rgb / rgba ํ•จ์ˆ˜
val magenta = rgb(1.0, 0.0, 1.0)
val magentaHalf = rgb(1.0, 0.0, 1.0, 0.5)

// 16์ง„์ˆ˜ ์ฝ”๋“œ (์ •์ˆ˜ ๋˜๋Š” ๋ฌธ์ž์—ด, ๋ฌธ์ž์—ด์˜ # ์€ ์ƒ๋žต ๊ฐ€๋Šฅ)
val c1 = ColorRGBa.fromHex(0xffc0cb)
val c2 = ColorRGBa.fromHex("#ffc0cb")

3.2 fill ยท stroke ยท ์„  ์Šคํƒ€์ผ

Drawer๊ฐ€ ์œ ์ง€ํ•˜๋Š” DrawStyle ์ค‘ ๋„ํ˜•๊ณผ ๊ด€๋ จ๋œ ์†์„ฑ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค(๊ณต์‹ ๊ฐ€์ด๋“œ์˜ ํ‘œ).

์†์„ฑํƒ€์ž…๊ธฐ๋ณธ๊ฐ’์„ค๋ช…
fillColorRGBa?WHITE์ฑ„์›€ ์ƒ‰. null์ด๋ฉด ์ฑ„์šฐ์ง€ ์•Š์Œ
strokeColorRGBa?BLACK์„  ์ƒ‰. null์ด๋ฉด ์„ ์„ ๊ทธ๋ฆฌ์ง€ ์•Š์Œ
strokeWeightDouble1.0์„  ๊ตต๊ธฐ
smoothBooleantrue์œค๊ณฝ์„ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํ‘œ์‹œํ• ์ง€ ์—ฌ๋ถ€
lineCapLineCapBUTT์„  ๋ ๋ชจ์–‘
lineJoinLineJoinMITER๊บพ์ด๋Š” ๋ถ€๋ถ„ ๋ชจ์–‘
miterLimitDouble4.0miter ์ตœ๋Œ€ ๊ธธ์ด
shadeStyleShadeStyle?null์…ฐ์ด๋” ์Šคํƒ€์ผ (5์žฅ)
blendModeBlendModeOVER(๊ฐ€์ด๋“œ ํ‘œ ๊ธฐ์ค€)ํ•ฉ์„ฑ ๋ฐฉ์‹
clipRectangle?null์ง์‚ฌ๊ฐํ˜• ํด๋ฆฌํ•‘ ์˜์—ญ (4์žฅ)
extend {
    drawer.clear(ColorRGBa.fromHex("#1E2A78"))
    drawer.fill = ColorRGBa.fromHex("#FFC0CB")
    drawer.stroke = ColorRGBa.WHITE
    drawer.strokeWeight = 6.0
    drawer.lineJoin = LineJoin.ROUND
    drawer.rectangle(100.0, 100.0, 300.0, 200.0)
}

3.3 ์ƒ‰ ์กฐ์ž‘: shade ยท opacify ยท mix

  • shade(f): 1.0 ๋ฏธ๋งŒ์ด๋ฉด ์–ด๋‘ก๊ฒŒ, 1.0 ์ดˆ๊ณผ์ด๋ฉด ๋ฐ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค(๊ณต์‹ ์˜ˆ์ œ์—์„œ shade(1.0 + i / 15.0)๋กœ ๋ฐ์€ ํ†ค์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค).
  • opacify(f): ๋ถˆํˆฌ๋ช…๋„๋ฅผ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.
  • mix(a, b, t): ๋‘ ์ƒ‰์„ t(0~1) ๋น„์œจ๋กœ ์„ž์Šต๋‹ˆ๋‹ค.
extend {
    drawer.stroke = null
    val base = ColorRGBa.PINK
    val target = ColorRGBa.fromHex("#FC3549")

    for (i in 0..15) {
        val x = 35.0 + (700 / 16.0) * i
        drawer.fill = base.shade(i / 15.0)              // ์–ด๋‘์šด ํ†ค
        drawer.rectangle(x, 32.0, 700 / 16.0, 64.0)

        drawer.fill = base.shade(1.0 + i / 15.0)        // ๋ฐ์€ ํ†ค
        drawer.rectangle(x, 96.0, 700 / 16.0, 64.0)

        drawer.fill = base.opacify(i / 15.0)            // ํˆฌ๋ช…๋„ ๋ณ€ํ™”
        drawer.rectangle(x, 160.0, 700 / 16.0, 64.0)

        drawer.fill = mix(base, target, i / 15.0)       // ํ˜ผํ•ฉ
        drawer.rectangle(x, 224.0, 700 / 16.0, 64.0)
    }
}

3.4 ๋‹ค๋ฅธ ์ƒ‰ ๊ณต๊ฐ„

ColorRGBa ์™ธ์— ๋‹ค์Œ ์ƒ‰ ๊ณต๊ฐ„ ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ํด๋ž˜์Šค์„ค๋ช…
ColorHSVa / ColorHSLa์ƒ‰์ƒยท์ฑ„๋„ยท๋ช…๋„(๋ฐ๊ธฐ)
ColorXSVa / ColorXSLaKuler ๋ฐฉ์‹์œผ๋กœ ์ƒ‰์ƒ ์ถ•์„ ๋ณด์ •ํ•œ HSV/HSL
ColorXYZa, ColorYxyaCIE ์ƒ‰ ๊ณต๊ฐ„
ColorLABa, ColorLCHABa, ColorLSHABaLAB ๊ณ„์—ด
ColorLUVa, ColorLCHUVa, ColorLSHUVaLUV ๊ณ„์—ด
ColorATVaColoroid (๋ถ€๋ถ„ ๊ตฌํ˜„)
ColorOKLABaOKLab. ColorRGBa.toOKLABa()๋กœ ๋ณ€ํ™˜ํ•˜๋ฉฐ ๊ทธ๋ผ๋””์–ธํŠธ ํ”„๋ฆฌ์…‹์—์„œ ์‚ฌ์šฉ๋จ
  • ์ƒ‰์ƒ ๊ฐ๋„(hue)๋Š” 0~360๋„์ž…๋‹ˆ๋‹ค. ์ƒ‰์ƒ ํ™˜์„ ๋„๋Š” ํŒ”๋ ˆํŠธ๋Š” HSV๋กœ ๋งŒ๋“ค๋ฉด ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.
extend {
    drawer.stroke = null
    for (i in 0 until 12) {
        drawer.fill = ColorHSVa(i * 30.0, 0.7, 0.9).toRGBa()   // hue 30๋„์”ฉ ์ฆ๊ฐ€
        drawer.circle(70.0 + i * 55.0, height / 2.0, 24.0)
    }
}
  • ๊ณต์‹ ๊ฐ€์ด๋“œ๋Š” Kuler ๋ฐฉ์‹์˜ XSV/XSL์„ "ํ™”๊ฐ€์—๊ฒŒ ๋” ์ ํ•ฉํ•œ ์ƒ‰ ๊ณต๊ฐ„"์œผ๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๋นจ๊ฐ•-์ดˆ๋ก, ํŒŒ๋ž‘-๋…ธ๋ž‘ ๋ณด์ƒ‰์ถ•์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐฐ์น˜๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

3.5 ์Šคํƒ€์ผ ์Šคํƒ: pushStyle ยท popStyle ยท isolated

  • ์Šคํƒ€์ผ์„ ๋ฐ”๊พผ ๋’ค ์›๋ž˜๋Œ€๋กœ ๋˜๋Œ๋ ค์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
drawer.pushStyle()
drawer.fill = ColorRGBa.PINK
drawer.rectangle(100.0, 100.0, 100.0, 100.0)
drawer.popStyle()

// ์Šคํƒ€์ผ๊ณผ ๋ณ€ํ™˜์„ ํ•จ๊ป˜ ์ €์žฅยท๋ณต์›
drawer.isolated {
    fill = ColorRGBa.PINK
    rectangle(100.0, 100.0, 100.0, 100.0)
}

3.6 ๋ธ”๋ Œ๋“œ ๋ชจ๋“œ

  • BlendMode ํ•ญ๋ชฉ์€ OVER, BLEND, ADD, SUBTRACT, MULTIPLY, REPLACE, REMOVE, MIN, MAX, SCREEN, OVERLAY, DARKEN, LIGHTEN, COLOR_DODGE, COLOR_BURN, HARD_LIGHT, SOFT_LIGHT, DIFFERENCE, EXCLUSION, HSL_HUE, HSL_SATURATION, HSL_COLOR, HSL_LUMINOSITY์ž…๋‹ˆ๋‹ค.
  • ๊ฐ€์‚ฐ ํ•ฉ์„ฑ(ADD)์€ ๊ฒน์นœ ๋ถ€๋ถ„์ด ๋ฐ์•„์ง€๋ฏ€๋กœ ๋น› ํ‘œํ˜„์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
extend {
    drawer.clear(ColorRGBa.BLACK)
    drawer.stroke = null
    drawer.isolated {
        drawStyle.blendMode = BlendMode.ADD
        val colors = listOf(ColorRGBa.RED, ColorRGBa.GREEN, ColorRGBa.BLUE)
        colors.forEachIndexed { i, c ->
            fill = c.opacify(0.8)
            circle(width / 2.0 + cos(seconds + i * 2.094) * 80.0,
                   height / 2.0 + sin(seconds + i * 2.094) * 80.0, 120.0)
        }
    }
}
  • 0.5.0 API ๋ฌธ์„œ๋Š” DrawStyle.blendMode๋ฅผ BlendMode?(๊ธฐ๋ณธ๊ฐ’ null)๋กœ ํ‘œ๊ธฐํ•˜๊ณ , ๊ฐ€์ด๋“œ ํ‘œ๋Š” ๊ธฐ๋ณธ๊ฐ’์„ OVER๋กœ ํ‘œ๊ธฐํ•ฉ๋‹ˆ๋‹ค. ํ‘œ๊ธฐ๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ํ•ฉ์„ฑ ๋ฐฉ์‹์ด ์ค‘์š”ํ•œ ์ฝ”๋“œ์—์„œ๋Š” ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

3.7 ๊ทธ๋ผ๋””์–ธํŠธ ์ฑ„์›€ (orx-shade-styles)

  • orx-shade-styles์˜ gradient ์ƒ์„ฑ๊ธฐ๋กœ ์„ ํ˜•ยท๋ฐฉ์‚ฌํ˜•ยท์›๋ฟ”ํ˜• ๊ทธ๋ผ๋””์–ธํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ GLSL ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ GPU์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋ผ๋””์–ธํŠธ๋Š” ๋„ํ˜•์˜ ๊ฒฝ๊ณ„ ์ƒ์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ 0~1 ์ขŒํ‘œ๊ณ„์—์„œ ์ •์˜๋ฉ๋‹ˆ๋‹ค.
// ์„ ํ˜•: ํšŒ์ „ํ•˜๋Š” ๋ฐฉํ–ฅ
drawer.shadeStyle = gradient<ColorRGBa> {
    stops[0.0] = ColorRGBa.PINK
    stops[0.8] = ColorRGBa.ORANGE
    stops[1.0] = ColorRGBa.RED
    linear {
        start = Polar(seconds * 60.0, 0.5).cartesian + 0.5
        end = Polar(seconds * 60.0 + 180.0, 0.5).cartesian + 0.5
    }
}
drawer.rectangle(80.0, 40.0, 200.0, 200.0)
drawer.circle(180.0, 340.0, 90.0)

// ๋ฐฉ์‚ฌํ˜•
drawer.shadeStyle = gradient<ColorRGBa> {
    stops[0.0] = ColorRGBa.PINK
    stops[1.0] = ColorRGBa.RED
    radial {
        radius = 0.5
        center = Vector2(cos(seconds), sin(seconds * 2.0)) * 0.5 + 0.5
    }
}

// ์›๋ฟ”ํ˜• + OKLab ๋ณด๊ฐ„
drawer.shadeStyle = gradient<ColorOKLABa> {
    stops[0.0] = ColorRGBa.PINK.toOKLABa()
    stops[1.0] = ColorRGBa.PURPLE.toOKLABa()
    conic { rotation = seconds * 60.0 }
}
  • quantization = 16์œผ๋กœ ์ƒ‰์„ ๋‹จ๊ณ„ํ™”ํ•  ์ˆ˜ ์žˆ๊ณ , levelWarpFunction, domainWarpFunction์— GLSL ํ•จ์ˆ˜ ๋ฌธ์ž์—ด์„ ๋„ฃ์–ด ๊ทธ๋ผ๋””์–ธํŠธ๋ฅผ ๋ณ€ํ˜•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Polar๋Š” org.openrndr.math์— ์žˆ์Šต๋‹ˆ๋‹ค. gradient์˜ import ๊ฒฝ๋กœ๋Š” 12์žฅ์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

3.8 ๋„ํ˜•๋งˆ๋‹ค ์ƒ‰์„ ๋‹ค๋ฅด๊ฒŒ ์ง€์ •ํ•˜๊ธฐ

  • OPENRNDR์—๋Š” ๋„ํ˜• ๊ฐ์ฒด๊ฐ€ ์ƒ‰์„ ๊ฐ–๋Š” ๊ฐœ๋…์ด ์—†์Šต๋‹ˆ๋‹ค. ์ƒ‰์€ ๊ทธ๋ฆฌ๊ธฐ ์ง์ „์˜ drawer.fill/drawer.stroke ์ƒํƒœ๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋„ํ˜•๋งˆ๋‹ค ์ƒ‰์ด ๋‹ค๋ฅด๋ฉด ๊ทธ๋ฆด ๋•Œ๋งˆ๋‹ค ์ƒ‰์„ ๋ฐ”๊ฟ” ์ค๋‹ˆ๋‹ค.
program {
    val palette = listOf("#F94144", "#F3722C", "#F9C74F", "#90BE6D", "#43AA8B", "#577590")
        .map { ColorRGBa.fromHex(it) }

    extend {
        drawer.clear(ColorRGBa.WHITE)
        drawer.stroke = null
        palette.forEachIndexed { i, color ->
            drawer.fill = color
            drawer.circle(90.0 + i * 120.0, height / 2.0, 50.0)
        }
    }
}

4. ๋„ํ˜•์„ ๋งˆ์Šคํฌ๋กœ ์“ฐ๋Š” ๋ฐฉ๋ฒ•

4.1 ๋ฐฉ์‹ ๋น„๊ต

OPENRNDR์˜ Drawer๋Š” ์ง์‚ฌ๊ฐํ˜• ํด๋ฆฌํ•‘ ํ•˜๋‚˜๋งŒ ์ง์ ‘ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ž„์˜์˜ ๋„ํ˜•์„ ๋งˆ์Šคํฌ๋กœ ์“ฐ๋ ค๋ฉด ์•„๋ž˜ ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ์‹๋งˆ์Šคํฌ ํ˜•ํƒœ๊ฒฐ๊ณผ์žฅ์ ํ•œ๊ณ„
A. drawStyle.clip์ง์‚ฌ๊ฐํ˜• 1๊ฐœ๋ž˜์Šคํ„ฐ๊ฐ€์žฅ ๋‹จ์ˆœํ•จ์ง์‚ฌ๊ฐํ˜•๋งŒ ๊ฐ€๋Šฅ
B. compound { intersection }์ž„์˜์˜ ๋ฒกํ„ฐ ๋„ํ˜•๋ฒกํ„ฐ๊ฒฐ๊ณผ๊ฐ€ ๋„ํ˜•์ด๋ฏ€๋กœ strokeยท์…ฐ์ด๋” ์ ์šฉ ๊ฐ€๋Šฅ, ๊ฒฝ๊ณ„๊ฐ€ ์„ ๋ช…ํ•จ์ž…๋ ฅ์ด ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋ฐ”๋€Œ๋ฉด CPU ๋น„์šฉ ์ฆ๊ฐ€
C. orx-compositor ๋ ˆ์ด์–ด + clip์ž„์˜์˜ ๊ทธ๋ฆฌ๊ธฐ ๊ฒฐ๊ณผ๋ž˜์Šคํ„ฐ์ด๋ฏธ์ง€ยทํ…์ŠคํŠธยท๋„ํ˜• ๋ชจ๋‘ ๋งˆ์Šคํฌ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ, ๋ธ”๋Ÿฌ ๋“ฑ ํ›„์ฒ˜๋ฆฌ ๊ฒฐํ•ฉ ์šฉ์ดORX ์˜์กด, ๋ ˆ์ด์–ด๋งˆ๋‹ค ์˜คํ”„์Šคํฌ๋ฆฐ ๋ฒ„ํผ ์‚ฌ์šฉ
D. RenderTarget 2์žฅ + ShadeStyle ํ•ฉ์„ฑ์ž„์˜์˜ ๊ทธ๋ฆฌ๊ธฐ ๊ฒฐ๊ณผ๋ž˜์Šคํ„ฐํ•ฉ์„ฑ์‹์„ ์ง์ ‘ ์ œ์–ด(๋ฐ˜์ „, ๋ถ€๋“œ๋Ÿฌ์šด ๊ฒฝ๊ณ„, ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋งˆ์Šคํฌ)์ฝ”๋“œ๋Ÿ‰์ด ๋งŽ์Œ
E. ํ”„๋ž˜๊ทธ๋จผํŠธ ์…ฐ์ด๋” SDF ๋งˆ์Šคํฌ์ˆ˜์‹์œผ๋กœ ์ •์˜๋œ ๋„ํ˜•๋ž˜์Šคํ„ฐ๋ฒ„ํผ๊ฐ€ ํ•„์š” ์—†๊ณ  GPU์—์„œ ์ฒ˜๋ฆฌ๋˜๋ฉฐ ๊ฒฝ๊ณ„ ์•ˆํ‹ฐ์•จ๋ฆฌ์–ด์‹ฑ์ด ์ข‹์Œ์ˆ˜์‹์œผ๋กœ ํ‘œํ˜„๋˜๋Š” ๋„ํ˜•์— ํ•œ์ •
  • ์ฐธ๊ณ : DrawStyle์—๋Š” stencil ๊ด€๋ จ ์†์„ฑ(StencilStyle)์ด ์žˆ์œผ๋‚˜ ๊ฐ€์ด๋“œ์— ์‚ฌ์šฉ ์˜ˆ๊ฐ€ ์—†๊ณ , contour/shape ์ฑ„์›€์ด ์Šคํ…์‹ค ๋ฒ„ํผ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์ ์ด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์—์„œ ํ™•์ธ๋˜๋ฏ€๋กœ ์ด ๋ฌธ์„œ์—์„œ๋Š” ๋‹ค๋ฃจ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

4.2 ๋ฐฉ์‹ A: ์ง์‚ฌ๊ฐํ˜• ํด๋ฆฌํ•‘

fun main() = application {
    program {
        extend {
            drawer.stroke = null
            drawer.fill = ColorRGBa.PINK

            // ํด๋ฆฌํ•‘ ์˜์—ญ ์„ค์ •
            drawer.drawStyle.clip = Rectangle(100.0, 100.0, width - 200.0, height - 200.0)

            drawer.circle(
                cos(seconds) * width / 2.0 + width / 2.0,
                sin(seconds) * height / 2.0 + height / 2.0,
                200.0
            )

            // ํด๋ฆฌํ•‘ ํ•ด์ œ
            drawer.drawStyle.clip = null
        }
    }
}
  • ํด๋ฆฌํ•‘์„ ํ•ด์ œํ•˜์ง€ ์•Š์œผ๋ฉด ์ดํ›„์˜ ๋ชจ๋“  ๊ทธ๋ฆฌ๊ธฐ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. drawer.isolated { } ์•ˆ์—์„œ ์„ค์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ณต์›๋ฉ๋‹ˆ๋‹ค.

4.3 ๋ฐฉ์‹ B: Boolean ๊ต์ง‘ํ•ฉ์œผ๋กœ ๋ฒกํ„ฐ ๋งˆ์Šคํ‚น

๋งˆ์Šคํฌ ๋„ํ˜•๊ณผ ์ฝ˜ํ…์ธ  ๋„ํ˜•์˜ ๊ต์ง‘ํ•ฉ์„ ๊ตฌํ•˜๋ฉด "๋งˆ์Šคํฌ ์•ˆ์ชฝ์— ์žˆ๋Š” ๋ถ€๋ถ„๋งŒ ๋‚จ์€ ๋„ํ˜•"์ด ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ๋ฒกํ„ฐ์ด๋ฏ€๋กœ ์ดํ›„์— stroke๋ฅผ ๊ทธ๋ฆฌ๊ฑฐ๋‚˜ ์…ฐ์ด๋”๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

fun main() = application {
    configure { width = 800; height = 600 }
    program {
        val mask = star(Vector2(400.0, 300.0), 260.0, 110.0, 5).shape

        // ์ฝ˜ํ…์ธ : ์„ธ๋กœ ์ค„๋ฌด๋Šฌ 12๊ฐœ
        val stripes = List(12) { i ->
            Rectangle(40.0 + i * 60.0, 0.0, 30.0, height * 1.0).shape
        }

        // ์ค„๋ฌด๋Šฌ๋งˆ๋‹ค ๋งˆ์Šคํฌ์™€์˜ ๊ต์ง‘ํ•ฉ์„ ๊ณ„์‚ฐ (์ž…๋ ฅ์ด ๊ณ ์ •์ด๋ฏ€๋กœ ํ•œ ๋ฒˆ๋งŒ)
        val clipped = stripes.map { stripe ->
            compound {
                intersection {
                    shape(mask)
                    shape(stripe)
                }
            }
        }

        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.stroke = null
            clipped.forEachIndexed { i, pieces ->
                drawer.fill = mix(ColorRGBa.PINK, ColorRGBa.fromHex("#FC3549"), i / 11.0)
                drawer.shapes(pieces)
            }

            // ๋งˆ์Šคํฌ ์œค๊ณฝ์„  ํ‘œ์‹œ
            drawer.fill = null
            drawer.stroke = ColorRGBa.BLACK
            drawer.strokeWeight = 3.0
            drawer.shape(mask)
        }
    }
}
  • ๋งˆ์Šคํฌ๋ฅผ ์›€์ง์ด๋ ค๋ฉด extend ์•ˆ์—์„œ compound๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ค„๋ฌด๋Šฌ ์ˆ˜๊ฐ€ ๋งŽ์œผ๋ฉด ํ”„๋ ˆ์ž„ ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜๋ฏ€๋กœ, ์›€์ง์ด๋Š” ๋งˆ์Šคํฌ์—๋Š” C~E ๋ฐฉ์‹์ด ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

4.4 ๋ฐฉ์‹ C: orx-compositor ๋ ˆ์ด์–ด ๋งˆ์Šคํฌ

orx-compositor๋Š” ๋ ˆ์ด์–ด ๊ธฐ๋ฐ˜ ํ•ฉ์„ฑ DSL์ž…๋‹ˆ๋‹ค. ๊ณต์‹ ๊ฐ€์ด๋“œ์˜ ๋งˆ์Šคํ‚น ํŒจํ„ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ๋งˆ์Šคํฌ๊ฐ€ ๋  ๋ ˆ์ด์–ด๋ฅผ ๋จผ์ € ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
  2. ์ฝ˜ํ…์ธ  ๋ ˆ์ด์–ด์— blend(Normal()) { clip = true }๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
  3. ๋‘ ๋ ˆ์ด์–ด๋ฅผ ํ•˜๋‚˜์˜ ๋ถ€๋ชจ layer { }๋กœ ๋ฌถ์–ด ๋ฐฐ๊ฒฝ๊ณผ ๋จผ์ € ํ•ฉ์„ฑ๋˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 800; height = 600 }
    program {
        val composite = compose {
            // ๋ฐฐ๊ฒฝ
            draw { drawer.clear(ColorRGBa.PINK) }

            layer {
                // ๋งˆ์Šคํฌ ๋ ˆ์ด์–ด: ์›€์ง์ด๋Š” ์›
                layer {
                    draw {
                        drawer.fill = ColorRGBa.WHITE
                        drawer.stroke = null
                        drawer.circle(width / 2.0 + cos(seconds) * 150.0, height / 2.0, 200.0)
                    }
                }
                // ์ฝ˜ํ…์ธ  ๋ ˆ์ด์–ด: ๋งˆ์Šคํฌ(ํ•˜์œ„ ๋ ˆ์ด์–ด)๊ฐ€ ์žˆ๋Š” ๋ถ€๋ถ„์—๋งŒ ํ‘œ์‹œ๋จ
                layer {
                    blend(Normal()) { clip = true }
                    draw {
                        drawer.stroke = null
                        for (i in 0 until 20) {
                            drawer.fill = if (i % 2 == 0) ColorRGBa.BLACK else ColorRGBa.WHITE
                            drawer.rectangle(i * 40.0, 0.0, 40.0, height * 1.0)
                        }
                    }
                }
            }
        }

        extend {
            composite.draw(drawer)
        }
    }
}
  • ์ฝ˜ํ…์ธ  ๋ ˆ์ด์–ด draw { }๋Š” ๋งค ํ”„๋ ˆ์ž„ ์‹คํ–‰๋˜๊ณ , compose { } ๋ณธ๋ฌธ๊ณผ layer { } ๋ณธ๋ฌธ(๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ๋“ฑ)์€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  • ๋งˆ์Šคํฌ ๋ ˆ์ด์–ด ๋’ค์— post(ApproximateGaussianBlur()) { ... }์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ถ€๋“œ๋Ÿฌ์šด ๊ฒฝ๊ณ„์˜ ๋งˆ์Šคํฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ด๋“œ๊ฐ€ ํ›„์ฒ˜๋ฆฌ๋ฅผ ๋ ˆ์ด์–ด ๋‹จ์œ„๋กœ ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•œ ๊ตฌ์„ฑ์ด๋ฉฐ, ์ ์šฉ ์ˆœ์„œ(๋งˆ์Šคํฌ ํ›„์ฒ˜๋ฆฌ โ†’ ์ฝ˜ํ…์ธ  ํ•ฉ์„ฑ)๋Š” ์‹คํ–‰ํ•˜์—ฌ ํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.
  • ๊ฐ€์žฅ์ž๋ฆฌ๊ฐ€ ๊ฑฐ์น ๋ฉด layer(multisample = BufferMultisample.SampleCount(8)) { ... }๋กœ ๋ฉ€ํ‹ฐ์ƒ˜ํ”Œ๋ง์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

4.5 ๋ฐฉ์‹ D: RenderTarget ๋‘ ์žฅ๊ณผ ShadeStyle๋กœ ์ง์ ‘ ํ•ฉ์„ฑ

๋งˆ์Šคํฌ ๋„ํ˜•๊ณผ ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ๊ฐ ์˜คํ”„์Šคํฌ๋ฆฐ ๋ฒ„ํผ์— ๊ทธ๋ฆฐ ๋’ค, ์…ฐ์ด๋”์—์„œ ์ฝ˜ํ…์ธ ์˜ RGB + ๋งˆ์Šคํฌ์˜ ์•ŒํŒŒ๋กœ ํ•ฉ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ•ฉ์„ฑ์‹์„ ์ง์ ‘ ์“ฐ๋ฏ€๋กœ ๋งˆ์Šคํฌ ๋ฐ˜์ „, ๋ถ€๋“œ๋Ÿฌ์šด ๊ฒฝ๊ณ„, ๋‹ค์ค‘ ๋งˆ์Šคํฌ ๊ณฑ ๋“ฑ ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

fun main() = application {
    configure { width = 800; height = 600 }
    program {
        // contour๋ฅผ ๊ทธ๋ฆฌ๋ฏ€๋กœ depthBuffer ํ•„์ˆ˜
        val maskTarget = renderTarget(width, height) {
            colorBuffer()
            depthBuffer()
        }
        val contentTarget = renderTarget(width, height) {
            colorBuffer()
            depthBuffer()
        }

        val starMask = star(Vector2(400.0, 300.0), 260.0, 110.0, 5)

        val compositeStyle = shadeStyle {
            fragmentTransform = """
                vec2 uv = c_boundsPosition.xy;
                uv.y = 1.0 - uv.y;
                vec3 content = texture(p_content, uv).rgb;
                float m = texture(p_mask, uv).a;
                x_fill = vec4(content, m);
            """.trimIndent()
        }

        extend {
            // 1. ๋งˆ์Šคํฌ ๋ฒ„ํผ: ํˆฌ๋ช… ๋ฐฐ๊ฒฝ์— ํฐ์ƒ‰ ๋„ํ˜•
            drawer.isolatedWithTarget(maskTarget) {
                drawer.clear(ColorRGBa.TRANSPARENT)
                drawer.fill = ColorRGBa.WHITE
                drawer.stroke = null
                drawer.contour(starMask)
            }

            // 2. ์ฝ˜ํ…์ธ  ๋ฒ„ํผ: ๋ฐ˜๋“œ์‹œ ๋ถˆํˆฌ๋ช…ํ•˜๊ฒŒ ์ฑ„์›€
            drawer.isolatedWithTarget(contentTarget) {
                drawer.clear(ColorRGBa.BLACK)
                drawer.stroke = null
                for (i in 0 until 40) {
                    drawer.fill = ColorRGBa.PINK.shade(0.4 + 0.6 * (0.5 + 0.5 * sin(seconds + i * 0.3)))
                    drawer.rectangle(i * 20.0, 0.0, 20.0, height * 1.0)
                }
            }

            // 3. ํ•ฉ์„ฑ: ํ™”๋ฉด ํฌ๊ธฐ์˜ ์‚ฌ๊ฐํ˜•์— ํ•ฉ์„ฑ ์…ฐ์ด๋” ์ ์šฉ
            compositeStyle.parameter("content", contentTarget.colorBuffer(0))
            compositeStyle.parameter("mask", maskTarget.colorBuffer(0))

            drawer.clear(ColorRGBa.fromHex("#222222"))
            drawer.isolated {
                drawer.shadeStyle = compositeStyle
                drawer.fill = ColorRGBa.WHITE
                drawer.stroke = null
                drawer.rectangle(drawer.bounds)
            }
        }
    }
}

์ฃผ์˜ํ•  ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ์ฝ˜ํ…์ธ  ๋ฒ„ํผ๋ฅผ ๋ถˆํˆฌ๋ช…ํ•˜๊ฒŒ ๊ทธ๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค(clear(BLACK)). ๋ฐ˜ํˆฌ๋ช… ์ฝ˜ํ…์ธ ๋Š” ์•ŒํŒŒ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์— ๋”ฐ๋ผ ์ƒ‰์ด ์–ด๋‘์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜ํˆฌ๋ช… ์ฝ˜ํ…์ธ ๊ฐ€ ํ•„์š”ํ•˜๋ฉด C ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ์œ„ ์ฝ”๋“œ์˜ uv.y = 1.0 - uv.y;๋Š” ๊ณต์‹ ๊ฐ€์ด๋“œ์˜ ์ด๋ฏธ์ง€ ๋งคํ•‘ ์˜ˆ์ œ(c_boundsPosition์œผ๋กœ ํ…์Šค์ฒ˜ ์ขŒํ‘œ๋ฅผ ๋งŒ๋“ค ๋•Œ y๋ฅผ ๋’ค์ง‘๋Š” ๋ฐฉ์‹)๋ฅผ ๋”ฐ๋ฅธ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ์œ„์•„๋ž˜๋กœ ๋’ค์ง‘ํ˜€ ๋ณด์ด๋ฉด ์ด ์ค„์„ ์ œ๊ฑฐํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.
  • ๋ Œ๋” ํƒ€๊นƒ ํฌ๊ธฐ๊ฐ€ ์ฐฝ๊ณผ ๋‹ค๋ฅด๋ฉด ๊ฐ isolatedWithTarget ๋ธ”๋ก ์•ˆ์—์„œ ortho(rt)๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ Œ๋” ํƒ€๊นƒ๊ณผ ShadeStyle์€ program { } ์•ˆ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

4.6 ๋ฐฉ์‹ E: ํ”„๋ž˜๊ทธ๋จผํŠธ ์…ฐ์ด๋” SDF ๋งˆ์Šคํฌ

๋ฒ„ํผ ์—†์ด ์…ฐ์ด๋” ์•ˆ์—์„œ ๋„ํ˜•์˜ ๋ถ€ํ˜ธ ์žˆ๋Š” ๊ฑฐ๋ฆฌ ํ•จ์ˆ˜(SDF)๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ์•ŒํŒŒ๋ฅผ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ๊ฐํ˜• ํ•˜๋‚˜๋ฅผ ๊ทธ๋ฆฌ๋ฉด์„œ ๊ทธ ์•ˆ์— ์›โ†”๋‘ฅ๊ทผ ์‚ฌ๊ฐํ˜•์ด ๋ณ€ํ•˜๋Š” ๋ชจ์–‘์˜ ๋งˆ์Šคํฌ๋ฅผ ์ ์šฉํ•˜๋Š” ์˜ˆ์ž…๋‹ˆ๋‹ค.

fun main() = application {
    configure { width = 800; height = 600 }
    program {
        val sdfMask = shadeStyle {
            fragmentPreamble = """
                float rn_sdRoundBox(vec2 p, vec2 b, float r) {
                    vec2 q = abs(p) - b + r;
                    return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
                }
            """.trimIndent()

            fragmentTransform = """
                vec2 p = c_boundsPosition.xy - 0.5;                 // ์ค‘์‹ฌ์ด (0, 0)์ธ -0.5 ~ 0.5 ์ขŒํ‘œ
                float circleD = length(p) - 0.4;
                float boxD = rn_sdRoundBox(p, vec2(0.4), 0.08);
                float d = mix(circleD, boxD, 0.5 + 0.5 * sin(p_time)); // ์› โ†” ๋‘ฅ๊ทผ ์‚ฌ๊ฐํ˜•

                float aa = fwidth(d);                                // ํ™”๋ฉด ํ”ฝ์…€ ๊ธฐ์ค€ ๊ฒฝ๊ณ„ ํญ
                float m = 1.0 - smoothstep(-aa, aa, d);              // ์•ˆ์ชฝ 1, ๋ฐ”๊นฅ์ชฝ 0

                // ๋งˆ์Šคํฌ ์•ˆ์ชฝ์— ๊ทธ๋ฆด ๋‚ด์šฉ
                x_fill.rgb = mix(vec3(0.99, 0.75, 0.80), vec3(0.12, 0.16, 0.47), c_boundsPosition.y);
                x_fill.a *= m;                                       // ์•ŒํŒŒ๋กœ ๋งˆ์Šคํ‚น
            """.trimIndent()
        }

        extend {
            sdfMask.parameter("time", seconds)
            drawer.clear(ColorRGBa.WHITE)
            drawer.shadeStyle = sdfMask
            drawer.stroke = null
            drawer.rectangle(200.0, 100.0, 400.0, 400.0)
        }
    }
}
  • ๋งˆ์Šคํฌ๋Š” ๊ทธ๋ ค์ง€๋Š” ์‚ฌ๊ฐํ˜•์˜ ๊ฒฝ๊ณ„ ์ƒ์ž ์•ˆ์—์„œ๋งŒ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์Šคํฌ๊ฐ€ ์‚ฌ๊ฐํ˜•๋ณด๋‹ค ์ปค์ง€์ง€ ์•Š๊ฒŒ ์ขŒํ‘œ ๋ฒ”์œ„๋ฅผ ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • discard๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆํ‹ฐ์•จ๋ฆฌ์–ด์‹ฑ ์—†์ด ์ž˜๋ผ๋‚ผ ์ˆ˜ ์žˆ์œผ๋‚˜, ์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ smoothstep์œผ๋กœ ์•ŒํŒŒ๋ฅผ ์กฐ์ ˆํ•˜๋Š” ๋ฐฉ์‹์ด ๊ฒฝ๊ณ„ ํ’ˆ์งˆ ๋ฉด์—์„œ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ๋„ํ˜• ํ•˜๋‚˜๊ฐ€ SDF๋กœ ํ‘œํ˜„๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ(์ž„์˜์˜ ํด๋ฆฌ๊ณค ๋“ฑ)์—๋Š” B~D ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

5. ๋„ํ˜• ์•ˆ์— GLSL ์…ฐ์ด๋” ๋„ฃ๊ธฐ (ShadeStyle)

5.1 ShadeStyle์˜ ๊ฐœ๋…

  • ShadeStyle์€ OPENRNDR๊ฐ€ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด ๋‘” ์…ฐ์ด๋” ํ…œํ”Œ๋ฆฟ์— GLSL ์ฝ”๋“œ ์กฐ๊ฐ์„ ๋ผ์›Œ ๋„ฃ๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์™„์ „ํ•œ ์…ฐ์ด๋” ํ”„๋กœ๊ทธ๋žจ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ main() ๋‚ด๋ถ€์™€ ๊ทธ ์•ž๋ถ€๋ถ„์— ์ฝ”๋“œ๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณ€ํ™˜์€ ๋‘ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.
ํ”„๋กœํผํ‹ฐ์‹คํ–‰ ๋‹จ๊ณ„์šฉ๋„
vertexTransform์ •์  ์…ฐ์ด๋”์ง€์˜ค๋ฉ”ํŠธ๋ฆฌ ๋ณ€ํ˜•
fragmentTransformํ”„๋ž˜๊ทธ๋จผํŠธ ์…ฐ์ด๋”ํ”ฝ์…€ ์ƒ‰ ๋ณ€๊ฒฝ (๋„ํ˜• ์ฑ„์›€์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉ)
vertexPreamble / fragmentPreamblemain() ์•ž์‚ฌ์šฉ์ž ํ•จ์ˆ˜, in/out ๋ณ€์ˆ˜ ์„ ์–ธ
  • ์ ์šฉ์€ drawer.shadeStyle = ...๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ๊ทธ๋ ค์ง€๋Š” ๋ชจ๋“  ๋„ํ˜•(circle, rectangle, contour, shape, ์ด๋ฏธ์ง€, ํ…์ŠคํŠธ)์— ์ ์šฉ๋˜๋ฉฐ drawer.shadeStyle = null๋กœ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.

5.2 ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ์˜ˆ: ์ƒ‰ ๋ฎ์–ด์“ฐ๊ธฐ

extend {
    drawer.shadeStyle = shadeStyle {
        fragmentTransform = "x_fill.rgb = vec3(1.0, 0.0, 0.0);"
    }
    drawer.fill = ColorRGBa.PINK
    drawer.stroke = null
    drawer.rectangle(width / 2.0 - 200.0, height / 2.0 - 200.0, 400.0, 400.0)
}
  • x_fill์€ ์ฑ„์›€ ์ƒ‰, x_stroke๋Š” ์„  ์ƒ‰์„ ๋‹ด์€ vec4์ž…๋‹ˆ๋‹ค. ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๊ทธ ๊ฒฐ๊ณผ๊ฐ€ ์ตœ์ข… ์ถœ๋ ฅ์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
  • ์œ„ ์˜ˆ์ œ์—์„œ drawer.fill์ด PINK์—ฌ๋„ ์…ฐ์ด๋”๊ฐ€ RGB๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ๋นจ๊ฐ„์ƒ‰์ด ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค. x_fill.rgb๋งŒ ๋ฐ”๊พธ๋ฉด ์•ŒํŒŒ(๊ฐ€์žฅ์ž๋ฆฌ ์ฒ˜๋ฆฌ ๋“ฑ)๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

5.3 ์…ฐ์ด๋” ์–ธ์–ด์˜ ์ ‘๋‘์‚ฌ ๊ทœ์น™

์ ‘๋‘์‚ฌ๋ฒ”์œ„์˜๋ฏธ
u_์ „์ฒดDrawer๊ฐ€ ์ „๋‹ฌํ•˜๋Š” ์‹œ์Šคํ…œ ์œ ๋‹ˆํผ (u_fill, u_stroke, u_strokeWeight, u_viewDimensions ๋“ฑ)
a_vertex์ •์  ์†์„ฑ (a_position, a_normal, a_color)
va_fragment๋ณด๊ฐ„๋œ ์ •์  ์†์„ฑ (va_position, va_normal, va_color, va_texCoord0)
v_fragment๋ณด๊ฐ„๋œ ๊ฐ’ (v_worldPosition, v_viewPosition, v_clipPosition ๋“ฑ)
i_, vi_vertex / fragment์ธ์Šคํ„ด์Šค ์†์„ฑ
x_์ „์ฒด๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๊ฐ’ (x_fill, x_stroke, x_position ๋“ฑ)
p_์ „์ฒด์‚ฌ์šฉ์ž๊ฐ€ parameter()๋กœ ์ „๋‹ฌํ•œ ๊ฐ’
o_fragment์ถœ๋ ฅ ๊ฐ’ (ํ•ญ์ƒ vec4)
d_์ „์ฒด์…ฐ์ด๋” ์ •์˜
c_์ „์ฒด์ƒ์ˆ˜ (c_screenPosition, c_boundsPosition, c_boundsSize, c_contourPosition, c_element, c_instance)
  • ์‚ฌ์šฉ์ž ํ•จ์ˆ˜์˜ ์ด๋ฆ„์€ ์œ„ ์ ‘๋‘์‚ฌ(x_, p_, u_ ๋“ฑ)๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๊ณ , ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ์ด๋ฆ„๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก rn_ ๊ฐ™์€ ๊ณ ์œ  ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ด๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

5.4 ๋„ํ˜• ์•ˆ์—์„œ ์‚ฌ์šฉํ•  ์ขŒํ‘œ

๋„ํ˜• ์•ˆ์ชฝ์— ํŒจํ„ด์„ ๊ทธ๋ฆฌ๋ ค๋ฉด ์–ด๋–ค ์ขŒํ‘œ๋ฅผ ์“ธ์ง€ ๋จผ์ € ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ขŒํ‘œ๋ฒ”์œ„ํŠน์ง•
c_boundsPosition.xy๋„ํ˜•์˜ ๊ฒฝ๊ณ„ ์ƒ์ž ๊ธฐ์ค€ 0~1 (y=0์ด ์œ„์ชฝ)๋„ํ˜•์ด ์–ด๋””์— ์žˆ๋“ , ์–ผ๋งˆ๋‚˜ ํฌ๋“  ๊ฐ™์€ ํŒจํ„ด์ด ๋„ํ˜•์— ๋งž์ถฐ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค. ๋„ํ˜• ์•ˆ์ชฝ ๋ฌด๋Šฌ์— ๊ฐ€์žฅ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
c_boundsSize.xy๊ฒฝ๊ณ„ ์ƒ์ž ํฌ๊ธฐ(px)c_boundsPosition๊ณผ ๊ณฑํ•˜๋ฉด ๋„ํ˜• ๋‚ด ํ”ฝ์…€ ์ขŒํ‘œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋ฌด๋Šฌ์˜ ๋ฌผ๋ฆฌ์  ํฌ๊ธฐ๋ฅผ ๊ณ ์ •ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
c_screenPositionํ™”๋ฉด ์ขŒํ‘œ(์žฅ์น˜ ์ขŒํ‘œ)๋„ํ˜•์ด ์›€์ง์—ฌ๋„ ํŒจํ„ด์ด ํ™”๋ฉด์— ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค. ๋„ํ˜•์ด ๋งˆ์น˜ "์ฐฝ"์ฒ˜๋Ÿผ ํ™”๋ฉด์˜ ํŒจํ„ด์„ ๋ณด์—ฌ ์ฃผ๋Š” ํšจ๊ณผ๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
va_texCoord0์ •์  ์†์„ฑ ๋ณด๊ฐ„contour ์„ ์—์„œ๋Š” .x๊ฐ€ ์„  ํญ ๋ฐฉํ–ฅ์œผ๋กœ 0โ†’1 ๋ณ€ํ•ฉ๋‹ˆ๋‹ค(๊ณต์‹ ๊ฐ€์ด๋“œ).
c_contourPosition0 ~ contour ๊ธธ์ด์„ /contour๋ฅผ ๊ทธ๋ฆด ๋•Œ๋งŒ 0์ด ์•„๋‹™๋‹ˆ๋‹ค. ์„ ์„ ๋”ฐ๋ผ ํ๋ฅด๋Š” ํšจ๊ณผ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ฐ™์€ ์Šคํƒ€์ผ์„ ์„œ๋กœ ๋‹ค๋ฅธ ๋„ํ˜•์— ์ ์šฉํ•˜๋ฉด c_boundsPosition์ด ๋„ํ˜•๋งˆ๋‹ค 0~1๋กœ ์ •๊ทœํ™”๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

fun main() = application {
    configure { width = 960; height = 400 }
    program {
        val starShape = star(Vector2(780.0, 200.0), 140.0, 60.0, 5).shape
        val uvStyle = shadeStyle {
            fragmentTransform = """
                vec2 uv = c_boundsPosition.xy;
                x_fill.rgb = vec3(uv, 0.5);       // x โ†’ ๋นจ๊ฐ•, y โ†’ ์ดˆ๋ก
            """.trimIndent()
        }
        extend {
            drawer.clear(ColorRGBa.BLACK)
            drawer.shadeStyle = uvStyle
            drawer.stroke = null
            drawer.circle(170.0, 200.0, 130.0)
            drawer.rectangle(360.0, 80.0, 240.0, 240.0)
            drawer.shape(starShape)
        }
    }
}

5.5 ํŒŒ๋ผ๋ฏธํ„ฐ ์ „๋‹ฌ (Kotlin โ†’ GLSL)

  • parameter("์ด๋ฆ„", ๊ฐ’)์œผ๋กœ ์ „๋‹ฌํ•œ ๊ฐ’์€ GLSL์—์„œ p_์ด๋ฆ„์œผ๋กœ ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.
  • ์ง€์› ํƒ€์ž…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
Kotlin ํƒ€์ž…GLSL ํƒ€์ž…
Double(float)float
Vector2 / Vector3 / Vector4vec2 / vec3 / vec4
ColorRGBavec4
Matrix44mat4
ColorBuffer / DepthBuffersampler2D
BufferTexturesamplerBuffer
  • ShadeStyle์€ program { } ์•ˆ์—์„œ ํ•œ ๋ฒˆ ๋งŒ๋“ค๊ณ , ๋งค ํ”„๋ ˆ์ž„ style.parameter(...)๋กœ ๊ฐ’๋งŒ ๊ฐฑ์‹ ํ•˜๋Š” ๋ฐฉ์‹์ด ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๊ณต์‹ ๊ฐ€์ด๋“œ์˜ 3D ์˜ˆ์ œ๋„ ์ด ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 800; height = 500 }
    program {
        val waveStyle = shadeStyle {
            fragmentTransform = """
                float w = 0.5 + 0.5 * sin(p_frequency * c_boundsPosition.x * 6.2831853 + p_time);
                x_fill.rgb = mix(p_colorA.rgb, p_colorB.rgb, w);
            """.trimIndent()
        }
        extend {
            waveStyle.parameter("time", seconds)
            waveStyle.parameter("frequency", 4.0)
            waveStyle.parameter("colorA", ColorRGBa.PINK)
            waveStyle.parameter("colorB", ColorRGBa.fromHex("#1E2A78"))

            drawer.clear(ColorRGBa.WHITE)
            drawer.shadeStyle = waveStyle
            drawer.stroke = null
            drawer.circle(width / 2.0, height / 2.0, 200.0)
        }
    }
}

5.6 fragmentPreamble๋กœ ํ•จ์ˆ˜ ์„ ์–ธ (๋…ธ์ด์ฆˆ ์˜ˆ์ œ)

  • fragmentTransform์€ main() ์•ˆ์ชฝ์— ๋“ค์–ด๊ฐ€๋ฏ€๋กœ ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ•จ์ˆ˜, ์ƒ์ˆ˜, in ๋ณ€์ˆ˜๋Š” fragmentPreamble์— ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ์˜ˆ์ œ๋Š” ๊ฐ’ ๋…ธ์ด์ฆˆ(value noise)์™€ fBm์„ preamble์— ์ •์˜ํ•˜๊ณ , ๋ณ„ ๋ชจ์–‘ ์•ˆ์ชฝ์„ ํ๋ฅด๋Š” ๊ตฌ๋ฆ„ ๋ฌด๋Šฌ๋กœ ์ฑ„์›๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 800; height = 600 }
    program {
        val starShape = star(Vector2(400.0, 300.0), 260.0, 120.0, 5).shape

        val noiseStyle = shadeStyle {
            fragmentPreamble = """
                float rn_hash21(vec2 p) {
                    p = fract(p * vec2(123.34, 456.21));
                    p += dot(p, p + 45.32);
                    return fract(p.x * p.y);
                }
                float rn_vnoise(vec2 p) {
                    vec2 i = floor(p);
                    vec2 f = fract(p);
                    vec2 u = f * f * (3.0 - 2.0 * f);
                    float a = rn_hash21(i);
                    float b = rn_hash21(i + vec2(1.0, 0.0));
                    float c = rn_hash21(i + vec2(0.0, 1.0));
                    float d = rn_hash21(i + vec2(1.0, 1.0));
                    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
                }
                float rn_fbm(vec2 p) {
                    float v = 0.0;
                    float a = 0.5;
                    for (int i = 0; i < 5; i++) {
                        v += a * rn_vnoise(p);
                        p = p * 2.0 + vec2(17.0, 9.0);
                        a *= 0.5;
                    }
                    return v;
                }
            """.trimIndent()

            fragmentTransform = """
                vec2 uv = c_boundsPosition.xy;
                float t = p_time * 0.2;
                float n = rn_fbm(uv * 4.0 + vec2(t, -t));
                x_fill.rgb = mix(p_colorA.rgb, p_colorB.rgb, smoothstep(0.2, 0.8, n));
            """.trimIndent()

            parameter("colorA", ColorRGBa.fromHex("#1E2A78"))
            parameter("colorB", ColorRGBa.fromHex("#FFC0CB"))
        }

        extend {
            noiseStyle.parameter("time", seconds)
            drawer.clear(ColorRGBa.BLACK)
            drawer.shadeStyle = noiseStyle
            drawer.stroke = null
            drawer.shape(starShape)
        }
    }
}

5.7 ์„ (stroke)์— ์…ฐ์ด๋” ์ ์šฉ: ํ๋ฅด๋Š” ์ ์„ 

  • ์„ ์„ ๊ทธ๋ฆด ๋•Œ๋Š” x_stroke๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  c_contourPosition์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ์ œ๋Š” ๊ณก์„ ์„ ๋”ฐ๋ผ ์ด๋™ํ•˜๋Š” ์ ์„ ์ž…๋‹ˆ๋‹ค.
  • c_contourPosition์€ 0 ~ contour ๊ธธ์ด ๋ฒ”์œ„์ด๋ฏ€๋กœ, 0~1๋กœ ์ •๊ทœํ™”ํ•˜๋ ค๋ฉด contour์˜ ๊ธธ์ด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 960; height = 600 }
    program {
        val wave = contour {
            moveTo(Vector2(80.0, 300.0))
            curveTo(Vector2(300.0, 40.0), Vector2(600.0, 560.0), Vector2(880.0, 300.0))
        }

        val dashStyle = shadeStyle {
            fragmentTransform = """
                float s = c_contourPosition / p_len;                  // 0 ~ 1
                float dash = step(0.5, fract(s * 20.0 - p_time * 0.5));
                x_stroke.rgb = mix(p_colorA.rgb, p_colorB.rgb, s);
                x_stroke.a *= dash;
            """.trimIndent()
            parameter("len", wave.length)
            parameter("colorA", ColorRGBa.PINK)
            parameter("colorB", ColorRGBa.fromHex("#4CC9F0"))
        }

        extend {
            dashStyle.parameter("time", seconds)
            drawer.clear(ColorRGBa.fromHex("#111111"))
            drawer.shadeStyle = dashStyle
            drawer.fill = null
            drawer.stroke = ColorRGBa.WHITE
            drawer.strokeWeight = 14.0
            drawer.contour(wave)
        }
    }
}

5.8 ๋ฐฐ์น˜ ๊ทธ๋ฆฌ๊ธฐ์™€ c_element

  • ๋ฐฐ์น˜ ๊ทธ๋ฆฌ๊ธฐ์—์„œ๋Š” c_element๊ฐ€ ๋ฐฐ์น˜ ๋‚ด ์š”์†Œ์˜ ์ธ๋ฑ์Šค์ž…๋‹ˆ๋‹ค(๊ณต์‹ ๊ฐ€์ด๋“œ์˜ ์ƒ์ˆ˜ ์ •์˜). ์š”์†Œ๋งˆ๋‹ค ์œ„์ƒ์ด ๋‹ค๋ฅธ ์ƒ‰ ๋ณ€ํ™”๋ฅผ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
fun main() = application {
    configure { width = 800; height = 600 }
    program {
        val circles = List(60) {
            Circle(80.0 + (it % 10) * 70.0, 100.0 + (it / 10) * 80.0, 28.0)
        }
        val elementStyle = shadeStyle {
            fragmentTransform = """
                float phase = float(c_element) * 0.35;
                x_fill.rgb = 0.5 + 0.5 * cos(vec3(0.0, 2.0, 4.0) + p_time + phase);
            """.trimIndent()
        }
        extend {
            elementStyle.parameter("time", seconds)
            drawer.clear(ColorRGBa.BLACK)
            drawer.shadeStyle = elementStyle
            drawer.stroke = null
            drawer.circles(circles)
        }
    }
}

5.9 ์ด๋ฏธ์ง€๋ฅผ ๋„ํ˜• ์•ˆ์— ๋งคํ•‘ํ•˜๊ธฐ

  • ColorBuffer ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ…์Šค์ฒ˜๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋„ํ˜• ์•ˆ์— ์ฑ„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต์‹ ๊ฐ€์ด๋“œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.
program {
    val image = loadImage("data/images/cheeta.jpg")
    image.filter(MinifyingFilter.LINEAR_MIPMAP_NEAREST, MagnifyingFilter.LINEAR)

    extend {
        drawer.shadeStyle = shadeStyle {
            fragmentTransform = """
                vec2 texCoord = c_boundsPosition.xy;
                texCoord.y = 1.0 - texCoord.y;
                vec2 size = textureSize(p_image, 0);
                texCoord.x /= size.x/size.y;
                x_fill = texture(p_image, texCoord);
            """.trimIndent()
            parameter("image", image)
        }
        val shape = Circle(width / 2.0, height / 2.0, 110.0).shape
        drawer.translate(cos(seconds) * 100.0, sin(seconds) * 100.0)
        drawer.shape(shape)
    }
}
  • ์ด ์˜ˆ์ œ์—์„œ ๋„ํ˜•์€ ์ด๋ฏธ์ง€๋ฅผ ํ†ตํ•ด ๋ณด์ด๋Š” "์ฐฝ"์˜ ์—ญํ• ์„ ํ•˜๋ฉฐ, ์ด๋Š” ๋„ํ˜•์„ ๋งˆ์Šคํฌ๋กœ ์“ฐ๋Š” ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

5.10 orx-shade-styles ํ”„๋ฆฌ์…‹

  • 3.7์ ˆ์˜ gradient๋ฅผ ํฌํ•จํ•˜์—ฌ orx-shade-styles์—๋Š” ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด์ง„ ์Šคํƒ€์ผ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ง์ ‘ GLSL์„ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ์›ํ•˜๋Š” ํšจ๊ณผ๊ฐ€ ํ”„๋ฆฌ์…‹์œผ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.
  • ๊ทธ๋ผ๋””์–ธํŠธ ๋ฐ๋ชจ: https://github.com/openrndr/orx/tree/master/orx-shade-styles/src/jvmDemo/kotlin/gradients

5.11 ๋””๋ฒ„๊น…๊ณผ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์‹ค์ˆ˜

์ฆ์ƒ ยท ์›์ธ๋Œ€์‘
์…ฐ์ด๋” ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๋กœ ํ”„๋กœ๊ทธ๋žจ์ด ์‹œ์ž‘๋˜์ง€ ์•Š์Œ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— ShaderError.glsl์ด ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ์—์„œ OPENRNDR๊ฐ€ ์ƒ์„ฑํ•œ ์ตœ์ข… ์…ฐ์ด๋”์™€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณ€์ˆ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜๋„์ ์œผ๋กœ ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋ฅผ ๋„ฃ์–ด ์ตœ์ข… ์…ฐ์ด๋”๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๊ณต์‹ ๊ฐ€์ด๋“œ์— ์†Œ๊ฐœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
float์— ์ •์ˆ˜ ๋ฆฌํ„ฐ๋Ÿด ์‚ฌ์šฉ (1)GLSL์—์„œ๋Š” 1.0์ฒ˜๋Ÿผ ์†Œ์ˆ˜์ ์„ ๋ถ™์ž…๋‹ˆ๋‹ค.
p_์ด๋ฆ„์„ ์ฐพ์„ ์ˆ˜ ์—†์ŒKotlin์˜ parameter("์ด๋ฆ„", โ€ฆ)๊ณผ GLSL์˜ p_์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•˜๋ฉฐ, ์ฒซ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๊ฐ’์ด ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๋ฌธ์ž์—ด์— $๊ฐ€ ํฌํ•จ๋จKotlin์˜ """ ๋ฌธ์ž์—ด์€ $๋ฅผ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ํ•ด์„ํ•ฉ๋‹ˆ๋‹ค. GLSL์— $๋ฅผ ์“ฐ์ง€ ์•Š๋„๋ก ํ•˜๊ณ , ๊ฐ’์„ ์‚ฝ์ž…ํ•˜๋ ค๋ฉด ${๊ฐ’} ํ…œํ”Œ๋ฆฟ์„ ์˜๋„์ ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค(๊ณต์‹ gradient ์˜ˆ์ œ์˜ levelWarpFunction์ด ์ด ๋ฐฉ์‹์„ ์‚ฌ์šฉ).
๋งค ํ”„๋ ˆ์ž„ shadeStyle { }๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด ๋А๋ฆผ์Šคํƒ€์ผ์€ ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“ค๊ณ  parameter()๋กœ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
์…ฐ์ด๋”๋ฅผ ์ ์šฉํ•œ ๋’ค ๋‹ค๋ฅธ ๋„ํ˜•๋„ ์˜ํ–ฅ์„ ๋ฐ›์Œ๊ทธ๋ฆฐ ๋’ค drawer.shadeStyle = null๋กœ ํ•ด์ œํ•˜๊ฑฐ๋‚˜ drawer.isolated { } ์•ˆ์—์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
๋งˆ์Šคํฌ/ํ•ฉ์„ฑ ๊ฒฐ๊ณผ๊ฐ€ ๋’ค์ง‘ํž˜4.5์ ˆ์˜ uv.y = 1.0 - uv.y; ์ค„์„ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

6. ์ข…ํ•ฉ ์˜ˆ์ œ

์„ธ ์ข…๋ฅ˜์˜ ๋„ํ˜•์— ์„œ๋กœ ๋‹ค๋ฅธ ShadeStyle์„ ์ ์šฉํ•˜๋Š” ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

  • ์›: ์ค„๋ฌด๋Šฌ ์…ฐ์ด๋” (5.5์ ˆ ๋ฐฉ์‹)
  • ๋ณ„ Shape: ๋…ธ์ด์ฆˆ ์…ฐ์ด๋” (5.6์ ˆ ๋ฐฉ์‹)
  • ์‚ฌ๊ฐํ˜•: SDF ๋งˆ์Šคํฌ๊ฐ€ ์ ์šฉ๋œ ๊ทธ๋ผ๋””์–ธํŠธ (4.6์ ˆ ๋ฐฉ์‹)
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.shape.*
import kotlin.math.cos
import kotlin.math.sin

fun star(center: Vector2, outer: Double, inner: Double, tips: Int, rotationDeg: Double = -90.0): ShapeContour {
    val points = List(tips * 2) {
        val r = if (it % 2 == 0) outer else inner
        val a = Math.toRadians(rotationDeg) + it * Math.PI / tips
        center + Vector2(cos(a), sin(a)) * r
    }
    return ShapeContour.fromPoints(points, closed = true)
}

fun main() = application {
    configure {
        width = 960
        height = 400
    }
    program {
        val starShape = star(Vector2(480.0, 200.0), 150.0, 65.0, 5).shape

        // 1) ์ค„๋ฌด๋Šฌ (์›)
        val stripeStyle = shadeStyle {
            fragmentTransform = """
                float s = c_boundsPosition.x + c_boundsPosition.y;
                float w = step(0.5, fract(s * p_count - p_time * 0.3));
                x_fill.rgb = mix(p_colorA.rgb, p_colorB.rgb, w);
            """.trimIndent()
            parameter("count", 8.0)
            parameter("colorA", ColorRGBa.fromHex("#FFC0CB"))
            parameter("colorB", ColorRGBa.fromHex("#1E2A78"))
        }

        // 2) ๋…ธ์ด์ฆˆ (๋ณ„ Shape)
        val noiseStyle = shadeStyle {
            fragmentPreamble = """
                float rn_hash21(vec2 p) {
                    p = fract(p * vec2(123.34, 456.21));
                    p += dot(p, p + 45.32);
                    return fract(p.x * p.y);
                }
                float rn_vnoise(vec2 p) {
                    vec2 i = floor(p);
                    vec2 f = fract(p);
                    vec2 u = f * f * (3.0 - 2.0 * f);
                    return mix(
                        mix(rn_hash21(i), rn_hash21(i + vec2(1.0, 0.0)), u.x),
                        mix(rn_hash21(i + vec2(0.0, 1.0)), rn_hash21(i + vec2(1.0, 1.0)), u.x),
                        u.y);
                }
                float rn_fbm(vec2 p) {
                    float v = 0.0;
                    float a = 0.5;
                    for (int i = 0; i < 5; i++) {
                        v += a * rn_vnoise(p);
                        p = p * 2.0 + vec2(17.0, 9.0);
                        a *= 0.5;
                    }
                    return v;
                }
            """.trimIndent()
            fragmentTransform = """
                float t = p_time * 0.2;
                float n = rn_fbm(c_boundsPosition.xy * 4.0 + vec2(t, -t));
                x_fill.rgb = mix(p_colorA.rgb, p_colorB.rgb, smoothstep(0.2, 0.8, n));
            """.trimIndent()
            parameter("colorA", ColorRGBa.fromHex("#F94144"))
            parameter("colorB", ColorRGBa.fromHex("#F9C74F"))
        }

        // 3) SDF ๋งˆ์Šคํฌ + ๊ทธ๋ผ๋””์–ธํŠธ (์‚ฌ๊ฐํ˜•)
        val sdfStyle = shadeStyle {
            fragmentPreamble = """
                float rn_sdRoundBox(vec2 p, vec2 b, float r) {
                    vec2 q = abs(p) - b + r;
                    return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
                }
            """.trimIndent()
            fragmentTransform = """
                vec2 p = c_boundsPosition.xy - 0.5;
                float d = mix(length(p) - 0.4, rn_sdRoundBox(p, vec2(0.4), 0.08), 0.5 + 0.5 * sin(p_time));
                float aa = fwidth(d);
                x_fill.rgb = mix(vec3(0.26, 0.67, 0.55), vec3(0.34, 0.46, 0.56), c_boundsPosition.y);
                x_fill.a *= 1.0 - smoothstep(-aa, aa, d);
            """.trimIndent()
        }

        extend {
            val styles = listOf(stripeStyle, noiseStyle, sdfStyle)
            styles.forEach { it.parameter("time", seconds) }

            drawer.clear(ColorRGBa.fromHex("#111111"))
            drawer.stroke = null

            drawer.isolated {
                drawer.shadeStyle = stripeStyle
                drawer.circle(170.0, 200.0, 130.0)
            }
            drawer.isolated {
                drawer.shadeStyle = noiseStyle
                drawer.shape(starShape)
            }
            drawer.isolated {
                drawer.shadeStyle = sdfStyle
                drawer.rectangle(680.0, 70.0, 260.0, 260.0)
            }
        }
    }
}

7. ์ž์ฃผ ์“ฐ๋Š” ํŒจํ„ด ์š”์•ฝ

ํ•˜๊ณ  ์‹ถ์€ ์ผ์‚ฌ์šฉํ•  ๊ธฐ๋Šฅ
์ฑ„์›€๋งŒ / ์„ ๋งŒ ๊ทธ๋ฆฌ๊ธฐfill = null ๋˜๋Š” stroke = null
๋„ํ˜•๋ณ„๋กœ ๋‹ค๋ฅธ ์ƒ‰๊ทธ๋ฆฌ๊ธฐ ์ง์ „์— drawer.fill ๋ณ€๊ฒฝ
์ž„์˜์˜ ๋‹ค๊ฐํ˜•ยท๋ณ„ShapeContour.fromPoints(points, closed = true)
๊ตฌ๋ฉ์ด ์žˆ๋Š” ๋„ํ˜•shape { contour { } contour { } } ๋˜๋Š” compound { difference { } }
๋„ํ˜•๋ผ๋ฆฌ ์ž๋ฅด๊ธฐยทํ•ฉ์น˜๊ธฐcompound { union / difference / intersection }
์ˆ˜์ฒœ ๊ฐœ์˜ ๋„ํ˜•drawer.circles(list), drawer.rectangles { }
์ง์‚ฌ๊ฐํ˜•์œผ๋กœ ์ž˜๋ผ๋‚ด๊ธฐdrawer.drawStyle.clip
์ž„์˜ ๋„ํ˜•/์ด๋ฏธ์ง€๋กœ ๋งˆ์Šคํ‚นorx-compositor์˜ blend(Normal()) { clip = true }
๋งˆ์Šคํฌ ํ•ฉ์„ฑ์‹์„ ์ง์ ‘ ์ œ์–ดRenderTarget 2์žฅ + ShadeStyle
์ˆ˜์‹์œผ๋กœ ์ •์˜๋œ ๋งˆ์Šคํฌํ”„๋ž˜๊ทธ๋จผํŠธ ์…ฐ์ด๋” SDF
๋„ํ˜• ์•ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฌด๋ŠฌfragmentTransform + p_time
์„ ์„ ๋”ฐ๋ผ ํ๋ฅด๋Š” ํšจ๊ณผc_contourPosition + x_stroke
๋ฐฐ์น˜ ๋„ํ˜•๋งˆ๋‹ค ๋‹ค๋ฅธ ์ƒ‰c_element
๊ทธ๋ผ๋””์–ธํŠธorx-shade-styles์˜ gradient<ColorRGBa> { }

8. ์„ฑ๋Šฅ ๊ด€์ ์˜ ์ •๋ฆฌ

  1. ๊ฐ์ฒด ์ƒ์„ฑ ์œ„์น˜: ShapeContour, Shape, compound ๊ฒฐ๊ณผ, ShadeStyle, RenderTarget์€ ์ž…๋ ฅ์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ํ•œ extend ๋ฐ”๊นฅ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฐฐ์น˜ ๊ทธ๋ฆฌ๊ธฐ: ๊ฐ™์€ ์ข…๋ฅ˜์˜ ๋„ํ˜•์ด ์ˆ˜๋ฐฑ ๊ฐœ ์ด์ƒ์ด๋ฉด circles/rectangles ๋ฐฐ์น˜ API๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  3. ๋งˆ์Šคํฌ ๋ฐฉ์‹ ์„ ํƒ: ์ •์ ์ธ ๋งˆ์Šคํฌ๋Š” B(๋ฒกํ„ฐ ๊ต์ง‘ํ•ฉ)๋กœ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•˜๊ณ , ์›€์ง์ด๋Š” ๋งˆ์Šคํฌ๋Š” C~E ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  4. ์…ฐ์ด๋” ๋น„์šฉ: fBm์˜ ๋ฐ˜๋ณต ํšŸ์ˆ˜, ๋งˆ์Šคํฌ SDF์˜ ๋ณต์žก๋„๋Š” ๋„ํ˜•์ด ์ฐจ์ง€ํ•˜๋Š” ํ”ฝ์…€ ์ˆ˜์— ๋น„๋ก€ํ•˜์—ฌ ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ํฐ ๋„ํ˜•์— ๋ฌด๊ฑฐ์šด ์…ฐ์ด๋”๋ฅผ ์ ์šฉํ•  ๋•Œ๋Š” ๋ฐ˜๋ณต ํšŸ์ˆ˜๋ฅผ ์ค„์ž…๋‹ˆ๋‹ค.
  5. ์ƒํƒœ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€: shadeStyle, clip, blendMode๋Š” ๋‹ค์Œ ๊ทธ๋ฆฌ๊ธฐ์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. isolated { }๋กœ ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

9. ๋ฌธ์„œ์—์„œ ๋‹ค๋ฃจ์ง€ ์•Š์€ ํ•ญ๋ชฉ

  • ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, SVG ๊ทธ๋ฆฌ๊ธฐ (๋„ํ˜• ๋ฒ”์œ„ ๋ฐ–)
  • 3D ๋ฉ”์‹œ์™€ vertexTransform ๊ธฐ๋ฐ˜ ์ง€์˜ค๋ฉ”ํŠธ๋ฆฌ ๋ณ€ํ˜•
  • StencilStyle์„ ์ด์šฉํ•œ ์Šคํ…์‹ค ๋งˆ์Šคํ‚น (๊ฐ€์ด๋“œ์— ์˜ˆ์ œ๊ฐ€ ์—†์–ด ์ œ์™ธ)
  • Compute shader, ์ด๋ฏธ์ง€ ํ•„ํ„ฐ ์ฒด์ธ(orx-fx์˜ ๋ธ”๋Ÿฌ ๋“ฑ ํ›„์ฒ˜๋ฆฌ ์ƒ์„ธ)

10. ์ฐธ๊ณ  ์ž๋ฃŒ

๊ณต์‹ ๊ฐ€์ด๋“œ (guide.openrndr.org)

๋ฆด๋ฆฌ์ฆˆยทAPI


11. 0.5.0 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ ํ™•์ธํ•  ์ 

  • 0.4.x ์ฝ”๋“œ์—์„œ ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ ๋ถ€๋ถ„์€ ๋Œ€์ฒด๋กœ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜์ง€๋งŒ, ๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ๋งŒ์œผ๋กœ ์ด๋ฅผ ๋ณด์ฆํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด API ๋ฌธ์„œ์—์„œ ํ•ด๋‹น ํ•จ์ˆ˜์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.
  • ์ฐฝ ์ƒ์„ฑ์ด SDL3 ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ”๋€Œ์—ˆ์œผ๋ฏ€๋กœ, ๊ทธ๋ฆฌ๊ธฐ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹Œ ์ฐฝ ๋™์ž‘(์ฝ˜ํ…์ธ  ์Šค์ผ€์ผ, ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋“ฑ)์—์„œ ์ฐจ์ด๊ฐ€ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. GLFW ๋ฐฑ์—”๋“œ๋Š” deprecated ์ƒํƒœ๋กœ ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค.
  • macOS์—์„œ Angle ๊ธฐ๋ฐ˜ GLES ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ shape/contour fill์ด ๊นจ์ ธ ๋ณด์ด๋˜ ๊ฒฝ์šฐ 0.5.0์—์„œ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

12. ๊ฒ€์ฆ ๋ฉ”๋ชจ

์ด ๋ฌธ์„œ๋ฅผ ๋ฐœํ–‰ํ•˜๊ธฐ ์ „์— ์‹คํ–‰ ํ™˜๊ฒฝ์—์„œ ํ™•์ธ์ด ํ•„์š”ํ•œ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.

ํ•ญ๋ชฉ๋‚ด์šฉ
์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๊ณต์‹ ๊ฐ€์ด๋“œ์˜ ์˜ˆ์ œ(2.2, 2.6.1, 2.6.3, 2.6.5, 3.3, 3.7, 4.2, 5.2, 5.9 ๋“ฑ)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋˜, ๋ณธ๋ฌธ์˜ ์กฐํ•ฉยทํ™•์žฅ ์ฝ”๋“œ(์ •๋‹ค๊ฐํ˜•/๋ณ„ ํ—ฌํผ, 4.3, 4.4, 4.5, 4.6, 5.6~5.8, 6์žฅ)๋Š” 0.5.0์—์„œ ์ง์ ‘ ์ปดํŒŒ์ผยท์‹คํ–‰ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
ORX import ๊ฒฝ๋กœgradient, compose, Normal, ApproximateGaussianBlur, hobbyCurve์˜ ํŒจํ‚ค์ง€๋Š” IDE ์ž๋™ import๋กœ ํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.
blendMode ๊ธฐ๋ณธ๊ฐ’๊ฐ€์ด๋“œ ํ‘œ๋Š” OVER, 0.5.0 API ๋ฌธ์„œ๋Š” BlendMode? = null์ž…๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
๋ Œ๋” ํƒ€๊นƒ y ๋ฐฉํ–ฅ4.5์ ˆ์˜ uv.y = 1.0 - uv.y;๋Š” ์ด๋ฏธ์ง€ ๋งคํ•‘ ์˜ˆ์ œ์˜ ๋ฐฉ์‹์„ ์ ์šฉํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ๋’ค์ง‘ํžˆ๋ฉด ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
compositor ํ›„์ฒ˜๋ฆฌ ์ˆœ์„œ4.4์ ˆ์—์„œ ๋งˆ์Šคํฌ ๋ ˆ์ด์–ด์— ๋ธ”๋Ÿฌ๋ฅผ ๊ฒฐํ•ฉํ–ˆ์„ ๋•Œ์˜ ๊ฒฐ๊ณผ๋Š” ์‹คํ–‰ํ•˜์—ฌ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
c_element๋ฐฐ์น˜ ๊ทธ๋ฆฌ๊ธฐ ์‹œ ์ธ๋ฑ์Šค ๋™์ž‘์€ ๊ฐ€์ด๋“œ์˜ ์ƒ์ˆ˜ ์ •์˜์— ๊ทผ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. circles(list)์—์„œ์˜ ๋™์ž‘์€ ์‹คํ–‰์œผ๋กœ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
API ๋ฌธ์„œ ๋ฒ„์ „ ํ‘œ๊ธฐAPI ๋ฌธ์„œ ์‚ฌ์ดํŠธ๋Š” 0.5.0-dev.2+6c5525b๋กœ ํ‘œ์‹œ๋˜๋ฉฐ, ์ด ์‹๋ณ„์ž๋Š” v0.5.0 ๋ฆด๋ฆฌ์ฆˆ ์ปค๋ฐ‹(6c5525b)๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.
profile
Coding Art with Blender / Three.js / p5.js / GLSL Shader / Processing / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€