์ด ๊ธ์ OPENRNDR 0.5.0 ๊ธฐ์ค 3D ์ํธ์ํฌ๋ฅผ ๋ค๋ฃน๋๋ค. ๊ณต์ ๊ฐ์ด๋(3D graphics, Shade styles, Transformations, Render targets)์ ORX(orx-mesh-generators, orx-camera)๋ฅผ ๊ทผ๊ฑฐ๋ก ์์ฑํ์ต๋๋ค.
OPENRNDR์ Blender๋ Three.js์ฒ๋ผ ์์ฑ๋ 3D ์์งยท์ฌ ๊ทธ๋ํยทGLTF ๋ก๋๋ฅผ ๊ธฐ๋ณธ ์ ๊ณตํ์ง ์์ต๋๋ค. ๋์ ๋ค์ ํ์ดํ๋ผ์ธ์ ํ๋ก๊ทธ๋๋ฐ์ผ๋ก ๊ตฌ์ฑํ์ฌ ์ ๋๋ ์ดํฐ๋ธ ์ํธยท์ ์ฐจ์ 3D ๊ทธ๋ํฝ์ ๋ง๋ค ์ ์์ต๋๋ค.
Kotlin ์ํ โ Mesh / VertexBuffer โ 3D transform โ Camera โ GLSL ShadeStyle โ (RenderTarget) โ ํ๋ฉด
๊ธฐ๋ณธ ๋ฉ์ ์์ฑ ยท ์นด๋ฉ๋ผ ยท VertexBuffer ยท ๋ฒํ ์ค ๋ณํ ยท ShadeStyle ์กฐ๋ช /๋์คํ ์ ยท ๊น์ด ๋ฒํผ ยท ์ ๋๋ ์ดํฐ๋ธ 3D ํจํด์ ๋ค๋ฃน๋๋ค.
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.camera.OrbitalCamera
import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.extra.meshgenerators.sphereMesh
import kotlin.math.cos
import kotlin.math.sin
orx-mesh-generators๋ positionยทnormalยทtexture coordinate๊ฐ ํฌํจ๋ VertexBuffer๋ฅผ ๋ฐํํฉ๋๋ค. ๋ํ ํจ์:
| ํจ์ | ์ค๋ช |
|---|---|
boxMesh(width, height, depth) | ์ง์ก๋ฉด์ฒด |
sphereMesh(sides, segments, radius) | UV ๊ตฌ |
cylinderMesh(sides, length, radius, ...) | ์๊ธฐ๋ฅ |
dodecahedronMesh(radius) | ์ ์ญ์ด๋ฉด์ฒด |
planeMesh(...) | ํ๋ฉด |
buildTriangleMesh { ... } | DSL๋ก ์ฌ๋ฌ ๋ฉ์ ์กฐํฉ |
val cube = boxMesh(140.0, 70.0, 10.0)
val sphere = sphereMesh(32, 32, 0.6)
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
drawer.vertexBuffer(sphere, DrawPrimitive.LINES) // ์์ด์ดํ๋ ์
DrawPrimitive.TRIANGLES, LINES, POINTS ๋ฑ์ ์ฌ์ฉํฉ๋๋ค.vertexBufferInstances)์ ๊ณ ๋ คํฉ๋๋ค.์ํ์ผ๋ก ์ง์ค๋ฉํธ๋ฆฌ๋ฅผ ๋ง๋ค ๋ vertexFormat + vertexBuffer๋ฅผ ์ง์ ์ฌ์ฉํฉ๋๋ค.
val geometry = vertexBuffer(vertexFormat {
position(3)
normal(3)
// textureCoordinate(2) // ํ์ ์
}, vertexCount)
geometry.put {
// write(Vector3(...)) ๋ฑ์ผ๋ก ์ฑ์
}
ํ๋ฆ์ ์ํ์ geometry ์์ฑ โ VertexBuffer โ GPU ๋ ๋๋ง์
๋๋ค. ๊ตฌ์ฒด๋ฅผ ์์ฑํ ๋ค ๋ฒํ
์ค ์์น๋ฅผ ํจ์๋ก ๋ณํํ๊ฑฐ๋, ShadeStyle์ vertexTransform์์ ๋ณํํ๋ ๋ฐฉ์์ด ์ ๋๋ ์ดํฐ๋ธ 3D์ ์ ๋ง์ต๋๋ค.
์ฌ๋ฌ ํ๋ฆฌ๋ฏธํฐ๋ธ๋ฅผ ํ๋์ ๋ฒํผ๋ก ํฉ์น ๋:
val mesh = buildTriangleMesh {
// rotate / translate ํ
box(2.0, 2.0, 2.0)
// translate ํ
sphere(16, 16, 1.0)
}
(์ ํํ ์๊ทธ๋์ฒยทํฌํผ๋ orx-mesh-generators ์์ค/๋ฐ๋ชจ๋ฅผ ์ฐธ๊ณ ํ์ธ์.)
drawer.perspective(60.0, width * 1.0 / height, 0.01, 1000.0)
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
drawer.translate(0.0, 0.0, -150.0)
drawer.rotate(Vector3.UNIT_X, seconds * 15.0 + 30.0)
drawer.rotate(Vector3.UNIT_Y, seconds * 5.0 + 60.0)
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
translate / rotate / scale์ model ํ๋ ฌ์ ์ ์ฉ๋ฉ๋๋ค.isolated { }๋ก ๋ฒ์๋ฅผ ์ ํํ๋ ๊ฒ์ด ์์ ํฉ๋๋ค.๋ง์ฐ์ค/ํค๋ณด๋๋ก ๊ถค๋๋ฅผ ๋๋ ์นด๋ฉ๋ผ๊ฐ ํ์ํ ๋:
val cam = Orbital()
cam.eye = -Vector3.UNIT_Z * 150.0
extend(cam)
extend {
drawer.fill = ColorRGBa.PINK
// shadeStyle ...
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
}
๋๋ OrbitalCamera๋ฅผ ์ง์ ์ฐ๋ ์ (๊ณต์ Shade styles ์์ ):
val camera = OrbitalCamera(Vector3.UNIT_Z, Vector3.ZERO)
extend(camera)
extend {
camera.rotate(0.2, 0.0)
// ...
}
Orbital์ OrbitalCamera + ์ปจํธ๋กค์ ๋ฌถ์ ํธ์ ํ์ฅ์
๋๋ค.| ํ๋ ฌ | Drawer ์ฐ์ฐ ์ |
|---|---|
| model | translate, rotate, scale |
| view | lookAt, ์นด๋ฉ๋ผ ํ์ฅ |
| projection | perspective, ortho |
Shape/ShapeContour๋ฅผ RenderTarget์ ๊ทธ๋ฆด ๋๋ depth(์คํ
์ค)๊ฐ ํ์ํฉ๋๋ค.depthWrite / depthTestPass๋ฅผ ์ค์ ํฉ๋๋ค.val rt = renderTarget(width, height) {
colorBuffer()
depthBuffer() // 3D ํ์
}
drawer.isolatedWithTarget(rt) {
drawer.clear(ColorRGBa.BLACK)
drawer.perspective(60.0, width * 1.0 / height, 0.01, 1000.0)
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
// ๋ฉ์ ๊ทธ๋ฆฌ๊ธฐ
}
drawer.image(rt.colorBuffer(0))
ShadeStyle์ ํ
ํ๋ฆฟ ์
ฐ์ด๋์ GLSL ์กฐ๊ฐ์ ๋ผ์ ๋ฃ๋ ๋ฐฉ์์
๋๋ค.x_position, x_normal ๋ฑ).x_fill, x_stroke).va_normal, v_worldPosition, v_worldNormal ๋ฑ์ด ์์ฃผ ์ฐ์
๋๋ค.fun main() = application {
configure {
multisample = WindowMultisample.SampleCount(4)
}
program {
val cube = boxMesh(140.0, 70.0, 10.0)
extend {
drawer.perspective(60.0, width * 1.0 / height, 0.01, 1000.0)
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
drawer.fill = ColorRGBa.PINK
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
float l = dot(va_normal, lightDir) * 0.4 + 0.5;
x_fill.rgb *= l;
""".trimIndent()
}
drawer.translate(0.0, 0.0, -150.0)
drawer.rotate(Vector3.UNIT_X, seconds * 15 + 30)
drawer.rotate(Vector3.UNIT_Y, seconds * 5 + 60)
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
}
}
}
๋ฒํ ์ค ์์น๋ฅผ ์๊ฐ์ ๋ฐ๋ผ ํ๋๋ ์์ ์ ๋๋ค. ์ ๋๋ ์ดํฐ๋ธ 3D์ ํต์ฌ ํจํด์ ๋๋ค.
fun main() = application {
program {
val sphere = sphereMesh(32, 32, 0.6)
val style = shadeStyle {
vertexTransform = """
vec3 p = x_position * 8.0 + p_seconds;
x_position.x += sin(p.y) * 0.1;
x_position.y += sin(p.z) * 0.1;
x_position.z += sin(p.x) * 0.1;
""".trimIndent()
fragmentTransform = """
vec3 c = sin(v_worldPosition) * 0.5 + 0.5;
x_fill = vec4(c, 1.0);
""".trimIndent()
}
val camera = OrbitalCamera(Vector3.UNIT_Z, Vector3.ZERO)
extend(camera)
extend {
camera.rotate(0.2, 0.0)
style.parameter("seconds", seconds)
drawer.shadeStyle = style
drawer.vertexBuffer(sphere, DrawPrimitive.LINES)
}
}
}
x_position์ ๋ฐ๊พธ๋ฉด ์ง์ค๋ฉํธ๋ฆฌ๊ฐ ๋ณํ๋ฉ๋๋ค.v_worldPosition์ผ๋ก ์๋ ์ขํ ๊ธฐ๋ฐ ์์ ์ค ์ ์์ต๋๋ค.parameter("seconds", seconds) โ GLSL p_seconds.| ์ด๋ฆ | ๋จ๊ณ | ์๋ฏธ |
|---|---|---|
x_position | vertex | ๋ณํ ๊ฐ๋ฅํ ์์น (์ด๊ธฐ๊ฐ a_position) |
x_normal | vertex | ๋ณํ ๊ฐ๋ฅํ ๋ฒ์ |
a_position / a_normal | vertex | ์๋ณธ ์์ฑ |
va_normal | fragment | ๋ณด๊ฐ๋ ๋ฒ์ |
v_worldPosition | fragment | ์๋ ์ขํ ์์น |
v_worldNormal | fragment | ์๋ ์ขํ ๋ฒ์ |
p_* | ์์ชฝ | ์ฌ์ฉ์ ํ๋ผ๋ฏธํฐ |
๋ฒ์ ์ ํฌ๊ฒ ๋ฐ๊พธ๋ฉด ์กฐ๋ช ์ด ๊นจ์ง ์ ์์ผ๋ฏ๋ก, ์์น๋ง ์์ง์ผ ๋๋ ํ์ํ๋ฉด ๋ฒ์ ์ ๋ค์ ๊ณ์ฐํ๋ ๋ฐฉ์์ ๊ณ ๋ คํฉ๋๋ค.
ShadeStyle ๋์ ๋ฒํผ๋ฅผ ๋ค์ ์ฑ์ธ ์๋ ์์ต๋๋ค.
geometry.put {
// ๋งค ํ๋ ์ ๋๋ ํ์ํ ๋๋ง
for (i in 0 until vertexCount) {
write(modifiedPosition)
// normal ๋ฑ
}
}
vertexTransform์ด ์ ๋ฆฌํ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.๊ฐ์ ๋ฉ์๋ฅผ ์ฌ๋ฌ ์์น/์์ผ๋ก ๊ทธ๋ฆด ๋:
val transforms = vertexBuffer(vertexFormat {
attribute("transform", VertexElementType.MATRIX44_FLOAT32)
// attribute("color", VertexElementType.VECTOR4_FLOAT32)
}, instanceCount)
// transforms.put { write(matrix) ... }
drawer.shadeStyle = shadeStyle {
vertexTransform = "x_modelMatrix *= i_transform;"
// fragmentTransform = "x_fill = vi_color;"
}
drawer.vertexBufferInstances(
listOf(mesh),
listOf(transforms),
DrawPrimitive.TRIANGLES,
transforms.vertexCount
)
i_ / vi_ ์ ๋์ฌ๋ก ์ธ์คํด์ค ์์ฑ์ ์ ๊ทผํฉ๋๋ค.vertexBuffer ํธ์ถ๋ก ๊ทธ๋ฆฌ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ํจ์จ์ ์
๋๋ค.fun main() = application {
configure {
width = 960
height = 540
multisample = WindowMultisample.SampleCount(4)
}
program {
val cube = boxMesh(80.0, 80.0, 80.0)
val sphere = sphereMesh(48, 48, 1.0)
val lightStyle = shadeStyle {
fragmentTransform = """
vec3 lightDir = normalize(vec3(0.4, 1.0, 0.3));
float l = max(dot(va_normal, lightDir), 0.0) * 0.6 + 0.4;
x_fill.rgb *= l;
""".trimIndent()
}
val distortStyle = shadeStyle {
vertexTransform = """
vec3 p = x_position * 6.0 + p_time;
x_position += 0.08 * vec3(sin(p.y), sin(p.z), sin(p.x));
""".trimIndent()
fragmentTransform = """
vec3 c = 0.5 + 0.5 * sin(v_worldPosition * 2.0 + p_time);
x_fill = vec4(c, 1.0);
""".trimIndent()
}
val cam = Orbital()
cam.eye = Vector3(0.0, 40.0, 220.0)
extend(cam)
extend {
lightStyle.parameter("time", seconds) // ํ์ ์
distortStyle.parameter("time", seconds)
drawer.clear(ColorRGBa.fromHex("#0d0d12"))
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
drawer.isolated {
drawer.shadeStyle = lightStyle
drawer.fill = ColorRGBa.fromHex("#FFC0CB")
drawer.translate(-90.0, 0.0, 0.0)
drawer.rotate(Vector3.UNIT_Y, seconds * 25.0)
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
}
drawer.isolated {
drawer.shadeStyle = distortStyle
drawer.fill = ColorRGBa.WHITE
drawer.translate(90.0, 0.0, 0.0)
drawer.scale(50.0)
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
}
}
}
}
| ํ๊ณ ์ถ์ ์ผ | ์ฌ์ฉํ ๊ธฐ๋ฅ |
|---|---|
| ๊ธฐ๋ณธ ๋ฐ์ค/๊ตฌ ๋ฉ์ | boxMesh, sphereMesh โ drawer.vertexBuffer |
| ๊ถค๋ ์นด๋ฉ๋ผ | Orbital() ๋๋ OrbitalCamera + extend |
| ์๋ค ๊ฐ๋ฆผ | depthWrite = true, depthTestPass = LESS_OR_EQUAL |
| ๊ฐ๋จํ ์กฐ๋ช | fragmentTransform์์ va_normal ๋ท ํ๋ก๋ํธ |
| ๋ฒํ ์ค ํ๋ค๊ธฐ/ํ๋ | vertexTransform์์ x_position ์์ + p_time |
| ์๋ ์ขํ ๊ธฐ๋ฐ ์ | v_worldPosition |
| ๊ฐ์ ๋ฉ์ ๋ค์ | vertexBufferInstances + ์ธ์คํด์ค ์์ฑ |
| ์คํ์คํฌ๋ฆฐ 3D | renderTarget { colorBuffer(); depthBuffer() } |
| ์ํ ๊ฒฉ๋ฆฌ | drawer.isolated { } |
boxMesh/sphereMesh, ShadeStyle, RenderTarget, ์ธ์คํด์ค ๋ฒํผ๋ ์
๋ ฅ์ด ๊ณ ์ ์ด๋ฉด extend ๋ฐ๊นฅ์์ ํ ๋ฒ๋ง ์์ฑํฉ๋๋ค.put๋ณด๋ค GPU vertexTransform์ด ์ ๋ฆฌํ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์ ์ ์๊ฐ ๋ง์์๋ก ์ฐจ์ด๊ฐ ํฝ๋๋ค.shadeStyle, depth ์ค์ , blendMode๋ ๋ค์ ๊ทธ๋ฆฌ๊ธฐ์ ๋จ์ต๋๋ค. isolated๋ก ๋ฒ์๋ฅผ ์ ํํฉ๋๋ค.WindowMultisample.SampleCount(4) ๋ฑ์ผ๋ก ๊ฐ์ฅ์๋ฆฌ๋ฅผ ๋ถ๋๋ฝ๊ฒ ํ ์ ์์ต๋๋ค(์ฑ๋ฅ ํธ๋ ์ด๋์คํ).orx-fx ํ์ฒ๋ฆฌ ์ฒด์ธ ์์ธ| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ์ฝ๋ ์คํ ์ฌ๋ถ | ๊ณต์ ๊ฐ์ด๋ 3D graphicsยทShade styles 3D mesh distortion ์์ ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํจ. ์ข ํฉ ์์ ยท์ธ์คํด์ฑ ์กฐํฉ ์ฝ๋๋ 0.5.0์์ ์ง์ ์คํ ๊ฒ์ฆํ์ง ์์ |
| ORX import | boxMesh, sphereMesh, Orbital, OrbitalCamera ํจํค์ง๋ IDE ์๋ import๋ก ํ์ธ |
| depth ๊ธฐ๋ณธ๊ฐ | 3D ์ depthWrite/depthTestPass๋ฅผ ๋ช
์์ ์ผ๋ก ์ค์ ํ ๊ฒ |
| ์นด๋ฉ๋ผ eye | Orbital์ ์ด๊ธฐ eye๋ ์ฅ๋ฉด์ ๋ง๊ฒ ์กฐ์ ํ์ |
c_element / ์ธ์คํด์ค | ๋ฐฐ์นยท์ธ์คํด์ค ์ธ๋ฑ์ค๋ ๊ฐ์ด๋ ์์ ์ ์์ ๊ทผ๊ฑฐ. ์คํ์ผ๋ก ํ์ธ ๊ถ์ฅ |
| API ๋ฌธ์ ๋ฒ์ | API ์ฌ์ดํธ ํ๊ธฐ๊ฐ 0.5.0-dev...์ฌ๋ v0.5.0 ๋ฆด๋ฆฌ์ฆ์ ์ปค๋ฐ์ด ์ผ์นํ๋ ๊ฒฝ์ฐ๊ฐ ์์ |
๊ณต์ ๊ฐ์ด๋
C170_TridimensionalGraphics.ktORX
Orbital, OrbitalCamera๋ฆด๋ฆฌ์ฆยทAPI