πŸ”§ β”  9. ν΄λž˜μŠ€μ™€ 데이터 클래슀 β€” μƒνƒœ 관리

BamgasiJMΒ·6일 μ „

OPENRNDR

λͺ©λ‘ 보기
9/17
post-thumbnail

μ§€κΈˆκΉŒμ§€ 배운 것듀은 λͺ¨λ‘ κ°’κ³Ό ν•¨μˆ˜λ₯Ό λ‹€λ£¨λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μ œλ„ˆλŸ¬ν‹°λΈŒ μ•„νŠΈμ—μ„œλŠ” μ—¬λŸ¬ 데이터λ₯Ό ν•˜λ‚˜λ‘œ λ¬Άμ–΄μ„œ 관리해야 ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄:

// λ‚˜μœ 방법: 데이터λ₯Ό ν©μ–΄λ†“μŒ
var x = 100.0
var y = 200.0
var vx = 1.5
var vy = -0.5
var radius = 10.0
var age = 0.0
var lifespan = 100.0

// 그러면 이 λͺ¨λ“  λ³€μˆ˜λ“€μ„ ν•¨μˆ˜μ— 전달해야 함
fun updateParticle(x: Double, y: Double, vx: Double, vy: Double, ...) {
    // λ³΅μž‘ν•¨!
}

이것을 ν•΄κ²°ν•˜λŠ” 방법이 ν΄λž˜μŠ€μž…λ‹ˆλ‹€:

// 쒋은 방법: 데이터λ₯Ό ν•œκ³³μ—
class Particle(
    var x: Double,
    var y: Double,
    var vx: Double,
    var vy: Double,
    var radius: Double = 10.0,
    var age: Double = 0.0,
    val lifespan: Double = 100.0
)

// μ‚¬μš©
val particle = Particle(100.0, 200.0, 1.5, -0.5)
particle.update()

이 κΈ€μ—μ„œλŠ”:

  • ν΄λž˜μŠ€κ°€ 무엇이고 μ™œ ν•„μš”ν•œκ°€
  • 데이터 클래슀의 κ°•λ ₯함
  • OPENRNDRμ—μ„œμ˜ μ‹€μ œ νŒ¨ν„΄

을 λ‹€λ£¨κ² μŠ΅λ‹ˆλ‹€.


클래슀의 기본

ν΄λž˜μŠ€λž€?

ν΄λž˜μŠ€λŠ” κ΄€λ ¨λœ 데이터와 ν•¨μˆ˜λ₯Ό ν•˜λ‚˜μ˜ λ‹¨μœ„λ‘œ λ¬ΆλŠ” κ²ƒμž…λ‹ˆλ‹€.

class Circle(
    var x: Double,
    var y: Double,
    var radius: Double
) {
    // μƒμ„±μž νŒŒλΌλ―Έν„°κ°€ μžλ™μœΌλ‘œ ν”„λ‘œνΌν‹°κ°€ 됨
    
    // λ©”μ„œλ“œ
    fun draw(drawer: Drawer) {
        drawer.circle(x, y, radius)
    }
    
    fun contains(px: Double, py: Double): Boolean {
        val dx = x - px
        val dy = y - py
        return dx * dx + dy * dy < radius * radius
    }
}

// μ‚¬μš©
val myCircle = Circle(100.0, 200.0, 50.0)
myCircle.draw(drawer)

μƒμ„±μžμ™€ ν”„λ‘œνΌν‹°

// μƒμ„±μž νŒŒλΌλ―Έν„°μ— val/var을 뢙이면 ν”„λ‘œνΌν‹°κ°€ 됨
class Point(
    val x: Double,  // val = 읽기만 κ°€λŠ₯ (λΆˆλ³€)
    var y: Double   // var = 읽고 μ“°κΈ° κ°€λŠ₯ (κ°€λ³€)
)

val p = Point(10.0, 20.0)
println(p.x)  // βœ… OK
p.x = 15.0    // ❌ μ—λŸ¬! valμ΄λ―€λ‘œ μž¬ν• λ‹Ή λΆˆκ°€

p.y = 25.0    // βœ… OK

데이터 클래슀 (Data Class)

데이터 ν΄λž˜μŠ€λŠ” "λ°μ΄ν„°λ§Œ λ‹΄λŠ”" ν΄λž˜μŠ€μž…λ‹ˆλ‹€. 코틀린이 μžλ™μœΌλ‘œ μœ μš©ν•œ λ©”μ„œλ“œλ₯Ό λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double = 10.0,
    val lifespan: Double = 100.0
)

μžλ™μœΌλ‘œ μƒμ„±λ˜λŠ” λ©”μ„œλ“œ

1. toString() β€” λ¬Έμžμ—΄ ν‘œν˜„

val p = Particle(Vector2(100.0, 200.0), Vector2(1.0, 0.5))

println(p)  // Particle(position=Vector2(100.0, 200.0), velocity=Vector2(1.0, 0.5), ...)

2. equals() β€” 값이 같은지 비ꡐ

val p1 = Particle(Vector2(100.0, 200.0), Vector2(1.0, 0.5))
val p2 = Particle(Vector2(100.0, 200.0), Vector2(1.0, 0.5))
val p3 = Particle(Vector2(150.0, 200.0), Vector2(1.0, 0.5))

println(p1 == p2)  // true (값이 κ°™μŒ)
println(p1 == p3)  // false (값이 닀름)

일반 ν΄λž˜μŠ€λŠ” 참쑰만 λΉ„κ΅ν•©λ‹ˆλ‹€:

class RegularParticle(val x: Double, val y: Double)

val r1 = RegularParticle(100.0, 200.0)
val r2 = RegularParticle(100.0, 200.0)

println(r1 == r2)  // false! (λ‹€λ₯Έ κ°μ²΄μ΄λ―€λ‘œ)

3. copy() β€” μΌλΆ€λ§Œ λ°”κΎΌ 볡사본

val original = Particle(
    position = Vector2(100.0, 200.0),
    velocity = Vector2(1.0, 0.5),
    radius = 10.0
)

// μœ„μΉ˜λ§Œ λ°”κΎΌ 볡사본
val moved = original.copy(position = Vector2(150.0, 250.0))

// 원본은 λ³€ν•˜μ§€ μ•ŠμŒ
println(original.position)  // Vector2(100.0, 200.0)
println(moved.position)     // Vector2(150.0, 250.0)

ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ—μ„œ 맀우 μ€‘μš”ν•©λ‹ˆλ‹€:

// κΈ°μ‘΄ μž…μžλ“€μ„ μ—…λ°μ΄νŠΈν•˜μ§€λ§Œ, 원본 보쑴
val updatedParticles = particles.map { p ->
    p.copy(
        position = p.position + p.velocity,
        age = p.age + 1.0
    )
}

p5.jsμ™€μ˜ 비ꡐ

p5.js: 객체 λ¦¬ν„°λŸ΄

let particle = {
    x: 100,
    y: 200,
    vx: 1.5,
    vy: -0.5,
    radius: 10,
    age: 0,
    
    update: function() {
        this.x += this.vx;
        this.y += this.vy;
        this.age++;
    },
    
    draw: function(ctx) {
        ctx.circle(this.x, this.y, this.radius);
    }
};

// μ‚¬μš©
particle.update();
particle.draw(ctx);

OPENRNDR: ν΄λž˜μŠ€μ™€ 데이터 클래슀

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double = 10.0,
    var age: Double = 0.0,
    val lifespan: Double = 100.0
) {
    fun update() {
        position += velocity
        age++
    }
    
    fun draw(drawer: Drawer) {
        drawer.circle(position.x, position.y, radius)
    }
}

// μ‚¬μš©
particle.update()
particle.draw(drawer)

OPENRNDR의 μž₯점:

  • νƒ€μž… μ•ˆμ „μ„±
  • μžλ™ toString(), equals(), copy()
  • 컴파일 νƒ€μž„ 검사

μ‹€μ „ 예제 1: μž…μž μ‹œμŠ€ν…œ

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double,
    var age: Double = 0.0,
    val lifespan: Double
) {
    val isAlive: Boolean
        get() = age < lifespan
    
    fun update() {
        position += velocity
        velocity *= 0.99  // 마찰
        age++
    }
    
    fun draw(drawer: Drawer) {
        val alpha = 1.0 - (age / lifespan)
        drawer.fill = ColorRGBa(1.0, 1.0, 1.0, alpha)
        drawer.circle(position.x, position.y, radius)
    }
}

program {
    val particles = mutableListOf<Particle>()
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 마우슀 μœ„μΉ˜μ—μ„œ μƒˆ μž…μž 생성
        repeat(3) {
            particles.add(Particle(
                position = mouse.position,
                velocity = Vector2(random(-2.0, 2.0), random(-2.0, 2.0)),
                radius = random(3.0, 8.0),
                lifespan = random(30.0, 60.0)
            ))
        }
        
        // μž…μž μ—…λ°μ΄νŠΈ 및 그리기
        particles.forEach { p ->
            p.update()
            p.draw(drawer)
        }
        
        // 죽은 μž…μž 제거
        particles.removeIf { !it.isAlive }
    }
}

κ°œμ„ : κ³„μ‚°λœ ν”„λ‘œνΌν‹° (Computed Property)

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double,
    var age: Double = 0.0,
    val lifespan: Double
) {
    // 읽기 μ „μš© ν”„λ‘œνΌν‹°
    val isAlive: Boolean
        get() = age < lifespan
    
    val alpha: Double
        get() = 1.0 - (age / lifespan)
    
    val speed: Double
        get() = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
}

μ‹€μ „ 예제 2: 선택 κ°€λŠ₯ν•œ λ„ν˜•

data class SelectableShape(
    val position: Vector2,
    val radius: Double,
    var isSelected: Boolean = false
) {
    fun contains(point: Vector2): Boolean {
        val dx = position.x - point.x
        val dy = position.y - point.y
        return dx * dx + dy * dy < radius * radius
    }
    
    fun draw(drawer: Drawer) {
        drawer.fill = if (isSelected) {
            ColorRGBa.RED
        } else {
            ColorRGBa.WHITE
        }
        drawer.circle(position.x, position.y, radius)
    }
    
    fun toggle() {
        isSelected = !isSelected
    }
}

program {
    val shapes = listOf(
        SelectableShape(Vector2(200.0, 200.0), 30.0),
        SelectableShape(Vector2(600.0, 200.0), 30.0),
        SelectableShape(Vector2(400.0, 600.0), 30.0)
    )
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 마우슀 클릭으둜 선택 ν† κΈ€
        if (mouse.buttonDown) {
            for (shape in shapes) {
                if (shape.contains(mouse.position)) {
                    shape.toggle()
                }
            }
        }
        
        // λͺ¨λ“  λ„ν˜• 그리기
        shapes.forEach { it.draw(drawer) }
    }
}

μ‹€μ „ 예제 3: μ›€μ§μ΄λŠ” μ—μ΄μ „νŠΈ

data class Agent(
    var position: Vector2,
    var target: Vector2? = null,
    val speed: Double = 2.0,
    val radius: Double = 10.0
) {
    fun moveTo(newTarget: Vector2) {
        target = newTarget
    }
    
    fun update() {
        target?.let { t ->
            val direction = (t - position).normalized
            position += direction * speed
            
            // λͺ©ν‘œ κ·Όμ²˜μ— λ„μ°©ν•˜λ©΄ 멈좀
            if ((position - t).length < 10.0) {
                target = null
            }
        }
    }
    
    fun draw(drawer: Drawer) {
        drawer.fill = if (target != null) ColorRGBa.CYAN else ColorRGBa.WHITE
        drawer.circle(position.x, position.y, radius)
        
        target?.let { t ->
            drawer.stroke = ColorRGBa.GRAY
            drawer.lineSegment(position, t)
        }
    }
}

program {
    val agent = Agent(Vector2(400.0, 400.0))
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 마우슀 클릭으둜 이동 λͺ…λ Ή
        if (mouse.buttonDown) {
            agent.moveTo(mouse.position)
        }
        
        agent.update()
        agent.draw(drawer)
    }
}

μ‹€μ „ 예제 4: μ€‘μ²©λœ 데이터 ꡬ쑰

data class Vector(
    var x: Double,
    var y: Double
) {
    operator fun plus(other: Vector): Vector {
        return Vector(x + other.x, y + other.y)
    }
}

data class Body(
    val position: Vector,
    var velocity: Vector,
    val mass: Double,
    val radius: Double
) {
    fun applyForce(force: Vector) {
        val acceleration = Vector(force.x / mass, force.y / mass)
        velocity = velocity + acceleration
    }
    
    fun update() {
        position.x += velocity.x
        position.y += velocity.y
    }
    
    fun draw(drawer: Drawer) {
        drawer.circle(position.x, position.y, radius)
    }
}

data class PhysicsSystem(
    val bodies: MutableList<Body> = mutableListOf(),
    var gravity: Vector = Vector(0.0, 0.1)
) {
    fun update() {
        bodies.forEach { body ->
            val gravitationalForce = Vector(0.0, body.mass * gravity.y)
            body.applyForce(gravitationalForce)
            body.update()
        }
    }
    
    fun draw(drawer: Drawer) {
        bodies.forEach { it.draw(drawer) }
    }
}

program {
    val physics = PhysicsSystem()
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 마우슀 클릭으둜 물체 μΆ”κ°€
        if (mouse.buttonDown) {
            physics.bodies.add(Body(
                position = Vector(mouse.position.x, mouse.position.y),
                velocity = Vector(random(-1.0, 1.0), random(-1.0, 1.0)),
                mass = random(1.0, 5.0),
                radius = random(5.0, 15.0)
            ))
        }
        
        physics.update()
        physics.draw(drawer)
    }
}

κΈ°λ³Έκ°’ (Default Parameters)

데이터 ν΄λž˜μŠ€μ—μ„œ 기본값을 μ„€μ •ν•˜λ©΄ 생성 μ‹œ μƒλž΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double = 10.0,      // κΈ°λ³Έκ°’
    var age: Double = 0.0,           // κΈ°λ³Έκ°’
    val lifespan: Double = 100.0     // κΈ°λ³Έκ°’
)

// μ‚¬μš©
val p1 = Particle(Vector2(0.0, 0.0), Vector2(1.0, 0.0))
// radius, age, lifespan은 κΈ°λ³Έκ°’ μ‚¬μš©

val p2 = Particle(
    Vector2(0.0, 0.0),
    Vector2(1.0, 0.0),
    radius = 20.0
)
// age, lifespan은 κΈ°λ³Έκ°’, radius만 λͺ…μ‹œ

이름 μžˆλŠ” 인자 (Named Arguments)

가독성을 μœ„ν•΄ μΈμžμ— 이름을 뢙일 수 μžˆμŠ΅λ‹ˆλ‹€.

// μœ„μΉ˜λ‘œλ§Œ 전달 (읽기 어렀움)
val p = Particle(Vector2(100.0, 200.0), Vector2(1.0, 0.5), 15.0, 0.0, 100.0)

// 이름과 ν•¨κ»˜ (읽기 쉬움)
val p = Particle(
    position = Vector2(100.0, 200.0),
    velocity = Vector2(1.0, 0.5),
    radius = 15.0,
    age = 0.0,
    lifespan = 100.0
)

// 기본값을 μ‚¬μš©ν•˜λ©° μΌλΆ€λ§Œ λͺ…μ‹œ
val p = Particle(
    position = Vector2(100.0, 200.0),
    velocity = Vector2(1.0, 0.5),
    radius = 15.0
)

λ©”μ„œλ“œ vs ν™•μž₯ ν•¨μˆ˜

λ©”μ„œλ“œ (클래슀 λ‚΄λΆ€)

data class Circle(
    val x: Double,
    val y: Double,
    val radius: Double
) {
    fun area(): Double {
        return PI * radius * radius
    }
    
    fun draw(drawer: Drawer) {
        drawer.circle(x, y, radius)
    }
}

ν™•μž₯ ν•¨μˆ˜ (클래슀 μ™ΈλΆ€)

fun Circle.circumference(): Double {
    return 2 * PI * radius
}

// μ‚¬μš©
val c = Circle(100.0, 100.0, 50.0)
println(c.area())           // λ©”μ„œλ“œ
println(c.circumference())  // ν™•μž₯ ν•¨μˆ˜

선택 κΈ°μ€€:

  • λ©”μ„œλ“œ: 클래슀의 핡심 λ™μž‘
  • ν™•μž₯ ν•¨μˆ˜: 선택적인 κΈ°λŠ₯, 라이브러리 ν•¨μˆ˜

κ³„μ‚°λœ ν”„λ‘œνΌν‹° (Computed Properties)

λ°μ΄ν„°λ‘œ κ³„μ‚°λ˜λŠ” ν”„λ‘œνΌν‹°λŠ” val둜 μ •μ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    var age: Double = 0.0,
    val lifespan: Double = 100.0
) {
    // κ³„μ‚°λœ ν”„λ‘œνΌν‹°: 맀번 계산됨
    val isAlive: Boolean
        get() = age < lifespan
    
    val alpha: Double
        get() = 1.0 - (age / lifespan)
    
    val speed: Double
        get() = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
}

// μ‚¬μš©
val p = Particle(Vector2(0.0, 0.0), Vector2(1.0, 0.0), 50.0)
println(p.isAlive)   // true
println(p.alpha)     // 0.5

데이터 클래슀 vs 일반 클래슀

κΈ°λŠ₯데이터 클래슀일반 클래슀
toString()μžλ™ μƒμ„±μˆ˜λ™ μ •μ˜
equals()μžλ™ 생성 (κ°’ 비ꡐ)μˆ˜λ™ μ •μ˜ (μ°Έμ‘° 비ꡐ)
copy()μžλ™ μƒμ„±μ—†μŒ
λ©”μ„œλ“œκ°€λŠ₯κ°€λŠ₯
μš©λ„λ°μ΄ν„° λ‹΄κΈ°λ³΅μž‘ν•œ λ™μž‘

μ‹€μ „ 예제 5: 격자 기반 κ²Œμž„

data class Cell(
    val x: Int,
    val y: Int,
    var state: Boolean = false  // false = 죽음, true = μ‚΄μ•„μžˆμŒ
) {
    fun draw(drawer: Drawer, size: Double) {
        if (state) {
            drawer.fill = ColorRGBa.WHITE
        } else {
            drawer.fill = ColorRGBa.GRAY
        }
        drawer.rectangle(x * size, y * size, size, size)
    }
}

data class Grid(
    val cols: Int,
    val rows: Int,
    val cells: MutableList<Cell> = MutableList(cols * rows) { i ->
        Cell(i % cols, i / cols)
    }
) {
    fun get(x: Int, y: Int): Cell? {
        if (x < 0 || x >= cols || y < 0 || y >= rows) return null
        return cells[x + y * cols]
    }
    
    fun toggle(x: Int, y: Int) {
        get(x, y)?.state = !(get(x, y)?.state ?: false)
    }
    
    fun draw(drawer: Drawer, cellSize: Double) {
        cells.forEach { it.draw(drawer, cellSize) }
    }
}

program {
    val grid = Grid(10, 10)
    val cellSize = 80.0
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 클릭으둜 μ…€ μƒνƒœ ν† κΈ€
        if (mouse.buttonDown) {
            val col = (mouse.position.x / cellSize).toInt()
            val row = (mouse.position.y / cellSize).toInt()
            grid.toggle(col, row)
        }
        
        grid.draw(drawer, cellSize)
    }
}

데이터 클래슀의 μž₯점 μš”μ•½

μž₯점섀λͺ…
κ°„κ²°μ„±κΈ°λ³Έ λ©”μ„œλ“œ μžλ™ 생성
μ•ˆμ „μ„±νƒ€μž… 체크, null μ•ˆμ „μ„±
비ꡐ값 기반 equals()
볡사copy()둜 λΆˆλ³€μ„± 지원
가독성λͺ…ν™•ν•œ 데이터 ꡬ쑰

λ‹€μŒ 단계: Object와 μ‹±κΈ€ν„΄

λ‹€μŒ κΈ€μ—μ„œλŠ” Object와 싱글턴을 λ‹€λ£Ήλ‹ˆλ‹€.

λ•Œλ‘œλŠ” 였직 ν•˜λ‚˜μ˜ μΈμŠ€ν„΄μŠ€λ§Œ ν•„μš”ν•œ κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€:

// 색상 νŒ”λ ˆνŠΈλŠ” ν”„λ‘œμ νŠΈ μ „μ²΄μ—μ„œ ν•˜λ‚˜λ§Œ ν•„μš”
object ColorPalette {
    val primary = ColorRGBa.RED
    val secondary = ColorRGBa.BLUE
    val accent = ColorRGBa.YELLOW
}

// μ‚¬μš©
extend {
    fill = ColorPalette.primary
}

μ‹±κΈ€ν„΄ νŒ¨ν„΄μ€ μ „μ—­ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜λŠ” κΉ”λ”ν•œ λ°©λ²•μž…λ‹ˆλ‹€.


μ—°μŠ΅ 문제

1. κΈ°λ³Έ 데이터 클래슀

data class Rectangle(
    val x: Double,
    val y: Double,
    val width: Double,
    val height: Double
)

val rect = Rectangle(100.0, 200.0, 150.0, 100.0)
println(rect)  // Rectangle(x=100.0, y=200.0, ...)

2. λ©”μ„œλ“œ μΆ”κ°€

data class Circle(
    val x: Double,
    val y: Double,
    val radius: Double
) {
    fun area(): Double = PI * radius * radius
    
    fun draw(drawer: Drawer) {
        drawer.circle(x, y, radius)
    }
}

3. κ³„μ‚°λœ ν”„λ‘œνΌν‹°

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    var age: Double = 0.0,
    val lifespan: Double = 100.0
) {
    val isAlive: Boolean
        get() = age < lifespan
    
    val alpha: Double
        get() = 1.0 - (age / lifespan)
}

4. OPENRNDR: κ°„λ‹¨ν•œ μž…μž μ‹œμŠ€ν…œ

data class Particle(
    var position: Vector2,
    var velocity: Vector2,
    val radius: Double = 5.0,
    var age: Double = 0.0,
    val lifespan: Double = 60.0
) {
    val isAlive: Boolean
        get() = age < lifespan
    
    fun update() {
        position += velocity
        velocity *= 0.99
        age++
    }
    
    fun draw(drawer: Drawer) {
        val alpha = 1.0 - (age / lifespan)
        drawer.fill = ColorRGBa(1.0, 1.0, 1.0, alpha)
        drawer.circle(position.x, position.y, radius)
    }
}

program {
    val particles = mutableListOf<Particle>()
    
    extend {
        clear(ColorRGBa.BLACK)
        
        // 마우슀 μœ„μΉ˜μ—μ„œ μž…μž 생성
        repeat(5) {
            particles.add(Particle(
                position = mouse.position,
                velocity = Vector2(random(-2.0, 2.0), random(-3.0, 0.0))
            ))
        }
        
        particles.forEach { p ->
            p.update()
            p.draw(drawer)
        }
        
        particles.removeIf { !it.isAlive }
    }
}

5. 심화: μƒν˜Έμž‘μš©ν•˜λŠ” 객체

μ§€λ‚œ "선택 κ°€λŠ₯ν•œ λ„ν˜•" 예제λ₯Ό ν™•μž₯ν•΄μ„œ:

  • λ„ν˜•μ„ λ“œλž˜κ·Έν•  수 있게 ν•˜κΈ°
  • 색상 λ³€κ²½ κΈ°λŠ₯ μΆ”κ°€
  • μ—¬λŸ¬ λ„ν˜• λ™μ‹œ 관리

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

0개의 λŒ“κΈ€