$30 off During Our Annual Pro Sale. View Details »

Let's Build a Cross-platform 3D Engine with Kotlin/Multiplatform

David
June 05, 2020

Let's Build a Cross-platform 3D Engine with Kotlin/Multiplatform

When creating a game, you want your game to be available on all platforms to target as many players as possible. Developing a game engine is a challenge: you have to deal with mathematical concepts, animations, player interactions... Creating an engine that runs on different platforms is even more challenging. Is Kotlin and Kotlin/Multiplatform could be the right tool to build this kind of engine? Let's check this out together.

Message for you, so you can already be aware what you can expect from this session:
Using existing Java game engines, you can create a game that runs on the JVM, in a browser or on Android and iOS. But it requires some complex tooling, when tools are available for the platform you're targeting.

This session is about the creation of a game engine that runs the same way in a browser, in a JVM, on iOS or Android.

(Conference For Kotliners - 05/06/2020 - Online)

David

June 05, 2020
Tweet

More Decks by David

Other Decks in Programming

Transcript

  1. LET'S BUILD A CROSS
    PLATFORM 3D ENGINE
    WITH KOTLIN/MULTIPLATFORM

    View Slide

  2. David
    Wursteisen

    View Slide

  3. "#$%&'($#&

    View Slide

  4. John Carmack

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. Game 

    development
    Kotlin
    Multiplatform

    View Slide

  9. You, at the end of 

    this conference
    Game 

    development
    Kotlin
    Multiplatform

    View Slide

  10. GAME
    DEVELOPMENT

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. public void render(float delta) {

    movePlayer(delta);

    moveEnemies(delta);

    moveWorld(delta);


    renderWorld();

    renderEnemies();

    renderPlayer();

    }

    View Slide

  17. public void render(float delta) {

    movePlayer(delta);

    moveEnemies(delta);

    moveWorld(delta);


    renderWorld();

    renderEnemies();

    renderPlayer();

    }

    View Slide

  18. public void render(float delta) {

    movePlayer(delta);

    moveEnemies(delta);

    moveWorld(delta);


    renderWorld();

    renderEnemies();

    renderPlayer();

    }

    View Slide

  19. public void render(float delta) {

    movePlayer(delta);

    moveEnemies(delta);

    moveWorld(delta);


    renderWorld();

    renderEnemies();

    renderPlayer();

    }
    60x

    seconds

    View Slide

  20. (0,0)
    x
    y

    View Slide

  21. PICKING A GAME ENGINE

    View Slide

  22. View Slide


  23. View Slide

  24. View Slide

  25. Hair Dash - http://cleancutgames.com/

    View Slide

  26. View Slide

  27. View Slide

  28. View Slide

  29. Core
    iOS
    GWT
    Android
    Desktop


    View Slide

  30. View Slide

  31. Hi all.
    My studio, Sickhead, did the port of Slay the Spire to Switch.
    We struggled for a bit trying to convert the Java to C++ code directly using existing third party
    tools. The resulting C++ code pretty much didn't work at all and took tons of manual changes to
    make work. After several months of failure taking that path we changed gears.
    We instead converted Java to C# which required much less manual fixes to make work and solved
    all the GC memory management issues. This means we had a C# version of LibGDX in there as
    well as all the game code. We then just added a new LibGDC backend with new OpenGL bindings
    and new sound, input implementations. At that point we had a PC port of Spire using C#.
    We then used our own mature C# IL to C++ cross compiler and runtime we developed to port XNA
    and MonoGame titles to generate binaries which run on Switch, PS4, XB1, and other platforms.
    While Java -> C# -> C++ was a crazy idea when we started... it actually works beautifully and
    makes perfect sense now.
    HTTPS://WWW.BADLOGICGAMES.COM/FORUM/VIEWTOPIC.PHP?F=11&T=29625

    View Slide

  32. Hi all.
    My studio, Sickhead, did the port of Slay the Spire to Switch.
    We struggled for a bit trying to convert the Java to C++ code directly using existing third party
    tools. The resulting C++ code pretty much didn't work at all and took tons of manual changes to
    make work. After several months of failure taking that path we changed gears.
    We instead converted Java to C# which required much less manual fixes to make work and solved
    all the GC memory management issues. This means we had a C# version of LibGDX in there as
    well as all the game code. We then just added a new LibGDC backend with new OpenGL bindings
    and new sound, input implementations. At that point we had a PC port of Spire using C#.
    We then used our own mature C# IL to C++ cross compiler and runtime we developed to port XNA
    and MonoGame titles to generate binaries which run on Switch, PS4, XB1, and other platforms.
    While Java -> C# -> C++ was a crazy idea when we started... it actually works beautifully and
    makes perfect sense now.
    HTTPS://WWW.BADLOGICGAMES.COM/FORUM/VIEWTOPIC.PHP?F=11&T=29625
    MY STUDIO, SICKHEAD, 

    DID THE PORT OF SLAY THE SPIRE TO SWITCH.

    WHILE JAVA -> C# -> C++ WAS A CRAZY IDEA
    WHEN WE STARTED... IT ACTUALLY WORKS
    BEAUTIFULLY AND MAKES PERFECT SENSE NOW.

    View Slide

  33. KOTLIN
    MULTIPLATFORM
    FEEDBACK LOOP

    View Slide

  34. LET'S DIVE IN
    BUILDING A 3D ENGINE

    View Slide

  35. void render(float delta) {
    }

    View Slide

  36. fun render(delta: float) {
    }

    View Slide

  37. KOTLIN/MULTIPLATFOM
    KOTLIN/JVM
    KOTLIN/NATIVE
    KOTLIN/JS
    KOTLIN/ANDROID

    View Slide

  38. Common
    Js
    Native
    Android
    Jvm

    View Slide

  39. KOTLIN/JVM KOTLIN/JS
    KOTLIN/ANDROID KOTLIN/NATIVE
    COMMON
    LIBGDX LIBGDX PLAYCANVAS RAYLIB

    View Slide

  40. JS NATIVE JVM

    View Slide

  41. KOTLIN/NATIVE
    KOTLIN/JS
    *.H CINTEROP *.KT
    *.D.TS DUKAT *.KT

    View Slide

  42. external open class ScrollbarComponentSystem(app: pc.Application) : ComponentSystem {
    override fun on(name: String, callback: HandleEvent, scope: Any): EventHandler
    override fun off(name: String, callback: HandleEvent, scope: Any): EventHandler
    override fun fire(name: Any, arg1: Any, arg2: Any, arg3: Any, arg4: Any): EventHandler
    override fun once(name: String, callback: HandleEvent, scope: Any): EventHandler
    override fun hasEvent(name: String): Boolean
    }

    View Slide

  43. external interface `T$8` {
    var volume: Number?
    get() = definedExternally
    set(value) = definedExternally
    var pitch: Number?
    get() = definedExternally
    set(value) = definedExternally
    var loop: Boolean?
    get() = definedExternally
    set(value) = definedExternally
    )

    View Slide

  44. View Slide

  45. View Slide

  46. OpenGL

    View Slide

  47. View Slide

  48. View Slide

  49. View Slide

  50. OpenGL ES was deprecated in iOS 12.

    View Slide

  51. View Slide

  52. VERTEX
    VERTEX
    VERTEX

    View Slide

  53. HTTPS://WWW.LABRI.FR/PERSO/NROUGIER/PYTHON-OPENGL/#MODERN-OPENGL
    VERTICES
    VERTEX

    SHADER
    FRAGMENT

    SHADER
    OpenGL (GPU)
    Kotlin (CPU)

    View Slide

  54. void main() {

    vec4 sum = vec4(0);


    float step = iResolution.y;

    // y

    for(float i = -area ; i <= area ; i += 1.) {

    // x

    for(float j = -area ; j <= area ; j += 1.) {

    float x = v_texCoords.x + i / iResolution.x;

    float y = v_texCoords.y + j / iResolution.y;

    sum += texture2D(u_texture, vec2(x, y)) * 0.005;

    }

    }

    gl_FragColor = texture2D(u_texture, v_texCoords) + sum;

    }
    OpenGL Shading Language (GLSL)

    View Slide

  55. View Slide

  56. View Slide

  57. View Slide

  58. View Slide

  59. BUILDING A 3D ENGINE
    BASED ON OpenGL

    View Slide

  60. KOTLIN/JVM KOTLIN/JS
    KOTLIN/ANDROID KOTLIN/NATIVE
    COMMON
    LWJGL OpenGL ES WebGL OpenGL

    View Slide

  61. WHAT YOU EXPECT

    View Slide

  62. WHAT YOU'LL GET

    View Slide

  63. View Slide

  64. A SHORT HIKE

    View Slide

  65. class GameExample : Game {
    override fun render(delta: Float) {
    }
    }
    Unit ?

    View Slide

  66. typealias Seconds = Float
    class GameExample : Game {
    override fun render(delta: Seconds) {
    }
    }

    View Slide

  67. interface CanMove {
    fun rotateX(angle: Float): CanMove
    }
    Radian or Degree ?

    View Slide

  68. typealias Degree = Float
    interface CanMove {
    fun rotateX(angle: Degree): CanMove
    }

    View Slide

  69. Y
    X
    Z

    View Slide

  70. 1. Translation
    2. Rotation
    3. Scale

    View Slide

  71. MATRIX

    View Slide

  72. MATRIX
    [ 1 | 0 | 0 | 0 ]

    [ 0 | 1 | 0 | 0 ]

    [ 0 | 0 | 1 | 0 ]

    [ 0 | 0 | 0 | 1 ]

    View Slide

  73. Scale
    MATRIX
    [ 1 | 0 | 0 | 0 ]

    [ 0 | 1 | 0 | 0 ]

    [ 0 | 0 | 1 | 0 ]

    [ 0 | 0 | 0 | 1 ]
    X
    Z
    Y
    Translation
    Rotation

    View Slide

  74. TRANSFORMATION = IDENTITY

    View Slide

  75. TRANSFORMATION = IDENTITY * TRANSLATION

    View Slide

  76. TRANSFORMATION = IDENTITY * TRANSLATION * TRANSLATION

    View Slide

  77. TRANSFORMATION = IDENTITY * TRANSLATION * TRANSLATION * ROTATION

    View Slide

  78. View Slide

  79. HTTPS://WWW.YOUTUBE.COM/WATCH?V=LJYDCYP0WG4

    View Slide

  80. data class Mat4(…) {
    // ...
    operator fun times(m: Mat4): Mat4 {
    val t = transpose(this)
    return Mat4(
    Float4(dot(t.x, m.x), dot(t.y, m.x), dot(t.z, m.x), dot(t.w, m.x)),
    Float4(dot(t.x, m.y), dot(t.y, m.y), dot(t.z, m.y), dot(t.w, m.y)),
    Float4(dot(t.x, m.z), dot(t.y, m.z), dot(t.z, m.z), dot(t.w, m.z)),
    Float4(dot(t.x, m.w), dot(t.y, m.w), dot(t.z, m.w), dot(t.w, m.w))
    )
    }
    }

    View Slide

  81. val result: Mat4 = Mat4(…) * Mat4(…)

    View Slide

  82. MATRIX
    [ 1 | 0 | 0 | 0 ]

    [ 0 | 1 | 0 | 0 ]

    [ 0 | 0 | 1 | 0 ]

    [ 0 | 0 | 0 | 1 ]
    How to access it?

    View Slide

  83. val mat: Mat4 = …
    val x = mat[0][4]
    val y = mat[1][4]
    val z = mat[2][4]



    View Slide

  84. data class Mat4(…) {
    // ...
    inline val scale: Float3
    get() = …
    inline val translation: Float3
    get() = …
    val rotation: Float3
    get() {

    }
    }



    View Slide

  85. data class Float3(var x: Float = 0.0f, var y: Float = 0.0f, var z: Float = 0.0f) {
    inline var r: Float
    get() = x
    set(value) {
    x = value
    }
    inline var g: Float
    get() = y
    set(value) {
    y = value
    }
    inline var b: Float
    get() = z
    set(value) {
    z = value
    }
    }

    View Slide

  86. data class Mat4(…) {
    // ...
    inline val scale: Float3
    get() = …
    inline val translation: Float3
    get() = …
    val rotation: Float3
    get() {

    }
    }

    View Slide

  87. data class Float3(
    var x: Float = 0.0f,
    var y: Float = 0.0f,
    var z: Float = 0.0f)
    val translation: Float3 = mat4.translation
    val (x, y, z) = translation

    View Slide

  88. CONVERTING
    INTO MULTIPLATFORM

    View Slide

  89. plugins {
    kotlin("multiplatform") version "1.3.60"
    }
    kotlin {
    js { }
    jvm { }
    sourceSets {
    val commonMain by getting {
    dependencies { implementation(kotlin("stdlib-common")) }
    }
    js().compilations["main"].defaultSourceSet {
    dependencies { implementation(kotlin("stdlib-js")) }
    }
    jvm().compilations["main"].defaultSourceSet {
    dependencies { implementation(kotlin("stdlib-jdk8")) }
    }
    }
    }

    View Slide

  90. inline fun pow(x: Float, y: Float) = StrictMath.pow(
    x.toDouble(),
    y.toDouble()
    ).toFloat()
    inline fun pow(x: Float, y: Float) = x.pow(y)
    java.lang.StrictMath

    View Slide

  91. KOTLIN-MATH.JAR (JVM)
    KOTLIN-MATH.JS (JS)
    KOTLIN-MATH.KLIB (NATIVE)

    View Slide

  92. BUILDING FOR
    MULTIPLATFORM

    View Slide

  93. interface GL {
    fun uniform1i(uniform: Uniform, data: Int)
    fun uniform2f(uniform: Uniform, first: Float, second: Float)
    fun uniform3f(uniform: Uniform, first: Float, second: Float, third: Float)
    // …
    }
    class WebGL(private val gl: WebGLRenderingContext) : GL {
    override fun uniform1i(uniform: Uniform, data: Int) {
    gl.uniform1i(uniform.uniformLocation, data)
    }
    override fun uniform2f(uniform: Uniform, first: Float, second: Float) {
    gl.uniform2f(uniform.uniformLocation, first, second)
    }
    override fun uniform3f(uniform: Uniform, first: Float, second: Float, third: Float) {
    gl.uniform3f(uniform.uniformLocation, first, second, third)
    }
    }
    COMMON

    View Slide

  94. interface GL {
    fun uniform1i(uniform: Uniform, data: Int)
    fun uniform2f(uniform: Uniform, first: Float, second: Float)
    fun uniform3f(uniform: Uniform, first: Float, second: Float, third: Float)
    // …
    }
    class WebGL(private val gl: WebGLRenderingContext) : GL {
    override fun uniform1i(uniform: Uniform, data: Int) {
    gl.uniform1i(uniform.uniformLocation, data)
    }
    override fun uniform2f(uniform: Uniform, first: Float, second: Float) {
    gl.uniform2f(uniform.uniformLocation, first, second)
    }
    override fun uniform3f(uniform: Uniform, first: Float, second: Float, third: Float) {
    gl.uniform3f(uniform.uniformLocation, first, second, third)
    }
    }
    COMMON
    KOTLIN/JS

    View Slide

  95. expect class GLContext {
    internal fun createContext(): GL
    }
    actual class GLContext {
    internal actual fun createContext(): GL {
    return LwjglGL(…)
    }
    }
    COMMON

    View Slide

  96. expect class GLContext {
    internal fun createContext(): GL
    }
    actual class GLContext {
    internal actual fun createContext(): GL {
    return LwjglGL(…)
    }
    }
    COMMON
    KOTLIN/JVM

    View Slide

  97. IN OTHER LANGUAGES, THIS CAN OFTEN BE ACCOMPLISHED BY BUILDING A SET OF
    INTERFACES IN THE COMMON CODE AND IMPLEMENTING THESE INTERFACES IN
    PLATFORM-SPECIFIC MODULES. HOWEVER, THIS APPROACH IS NOT IDEAL IN CASES
    WHEN YOU HAVE A LIBRARY ON ONE OF THE PLATFORMS THAT IMPLEMENTS THE
    FUNCTIONALITY YOU NEED, AND YOU'D LIKE TO USE THE API OF THIS LIBRARY DIRECTLY
    WITHOUT EXTRA WRAPPERS.

    View Slide

  98. IN OTHER LANGUAGES, THIS CAN OFTEN BE ACCOMPLISHED BY BUILDING A SET OF
    INTERFACES IN THE COMMON CODE AND IMPLEMENTING THESE INTERFACES IN
    PLATFORM-SPECIFIC MODULES. HOWEVER, THIS APPROACH IS NOT IDEAL IN CASES
    WHEN YOU HAVE A LIBRARY ON ONE OF THE PLATFORMS THAT IMPLEMENTS THE
    FUNCTIONALITY YOU NEED, AND YOU'D LIKE TO USE THE API OF THIS LIBRARY DIRECTLY
    WITHOUT EXTRA WRAPPERS.

    View Slide

  99. // Common
    expect class Uniform
    // JVM
    actual class Uniform(val address: Int)
    // JVM
    override fun uniform2f(uniform: Uniform, first: Float, second: Float) {
    glUniform2f(uniform.address, first, second)
    }
    COMMON
    KOTLIN/JVM
    1. Producing
    2. Consuming

    View Slide

  100. // Common
    expect class Uniform
    // JVM
    actual class Uniform(val address: Int)
    // JVM
    override fun uniform2f(uniform: Uniform, first: Float, second: Float) {
    glUniform2f(uniform.address, first, second)
    }
    COMMON
    KOTLIN/JVM
    1. Producing
    2. Consuming

    View Slide

  101. DRAWING FIRST TRIANGLE

    View Slide

  102. val vertices = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, vertices)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertPositions(),
    GL.STATIC_DRAW)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexPosition"),
    size = 3,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexPosition"))

    View Slide

  103. val vertices = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, vertices)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertPositions(),
    GL.STATIC_DRAW)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexPosition"),
    size = 3,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexPosition"))
    X Y Z

    View Slide

  104. val colors = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertColors(),
    GL.STATIC_DRAW)
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexColor"),
    size = 4,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexColor"))

    View Slide

  105. val colors = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertColors(),
    GL.STATIC_DRAW)
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexColor"),
    size = 4,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexColor"))
    Red Green Blue Alpha

    View Slide

  106. val verticesOrder = gl.createBuffer()
    gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, verticesOrder)
    gl.bufferData(GL.ELEMENT_ARRAY_BUFFER,
    mesh.verticesOrder.convertOrder(), GL.STATIC_DRAW)
    gl.drawElements(mesh.drawType.glType, mesh.verticesOrder.size,
    GL.UNSIGNED_SHORT, 0)

    View Slide

  107. A
    B
    C
    D
    E

    View Slide

  108. A
    B
    C
    D
    E

    View Slide

  109. val vertices = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, vertices)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertPositions(), GL.STATIC_DRAW)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexPosition"),
    size = 3,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexPosition"))
    val colors = gl.createBuffer()
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.bufferData(GL.ARRAY_BUFFER, mesh.vertices.convertColors(), GL.STATIC_DRAW)
    gl.bindBuffer(GL.ARRAY_BUFFER, colors)
    gl.vertexAttribPointer(
    index = shader.getAttrib("aVertexColor"),
    size = 4,
    type = GL.FLOAT,
    normalized = false,
    stride = 0,
    offset = 0
    )
    gl.enableVertexAttribArray(shader.getAttrib("aVertexColor"))
    val verticesOrder = gl.createBuffer()
    gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, verticesOrder)
    gl.bufferData(GL.ELEMENT_ARRAY_BUFFER, mesh.verticesOrder.convertOrder(), GL.STATIC_DRAW)
    gl.drawElements(mesh.drawType.glType, mesh.verticesOrder.size, GL.UNSIGNED_SHORT, 0)
    Vertices
    Colors
    Order

    View Slide

  110. attribute vec3 aVertexPosition;
    void main() {
    gl_Position = aVertexPosition;
    }

    View Slide

  111. attribute vec4 aVertexColor;
    void main() {
    gl_FragColor = aVertexColor;
    }

    View Slide

  112. Color interpolation

    View Slide

  113. Perspective Camera

    View Slide

  114. Perspective Camera

    (wide angle)

    View Slide

  115. Orthographic

    View Slide

  116. Perspective Camera

    Frustum

    View Slide

  117. Perspective Camera

    Frustum

    View Slide

  118. https://webglfundamentals.org/webgl/lessons/webgl-3d-camera.html

    View Slide

  119. https://webglfundamentals.org/webgl/lessons/webgl-3d-camera.html

    View Slide

  120. View Slide

  121. 0.1253255324554
    145343224242.12

    View Slide

  122. View Slide

  123. attribute vec3 aVertexPosition;
    void main() {
    gl_Position = aVertexPosition;
    }

    View Slide

  124. attribute vec4 aVertexPosition;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    void main() {
    gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    }

    View Slide

  125. attribute vec4 aVertexPosition;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    void main() {
    gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    }
    Change for every vertex
    Common for all vertices

    View Slide

  126. Try me

    View Slide

  127. PLATFORM
    DIFFERENCES

    View Slide

  128. interface Game {
    fun render(delta: Seconds)
    }

    View Slide

  129. override fun render(delta: Seconds) {
    // --- act ---
    model.rotateZ(delta * 20)
    }

    View Slide

  130. MOBILE APP

    VERSUS

    DESKTOP APP

    View Slide

  131. TOUCH

    VERSUS

    CLICK

    View Slide

  132. LEFT CONTROL

    VERSUS

    CONTROL

    View Slide

  133. WEB
    INPUTS
    DESKTOP
    INPUTS
    MOBILE
    INPUTS
    TARGET API

    View Slide

  134. IMPLEMENTATION
    DETAILS

    View Slide

  135. WEB JVM ANDROID

    View Slide

  136. FIXING PLATFORMS
    IMPLEMENTATION DETAILS IS HARD
    AND CAN GIVE YOU HEADACHE




    View Slide

  137. BUT ONE PLATFORM
    CAN HELP YOU TO WORK
    ON ANOTHER ONE.
    AND THAT'S COOL

    View Slide

  138. GRADLEW -T
    GRADLE BROWSER
    WEBPACK
    Reload on change Reload on change

    View Slide

  139. SPECTORJS

    View Slide

  140. SKELETON BASED
    ANIMATIONS

    View Slide

  141. View Slide

  142. View Slide

  143. View Slide

  144. View Slide

  145. class DemoSuzanne : Game {
    private val model: Drawable = fileHandler.get("suzanne.protobuf")
    fun render(delta: Seconds) {
    model.draw()
    }
    }
    Usage
    Synchronous loading

    View Slide

  146. class DemoSuzanne : Game {
    private val model: Drawable by fileHandler.get("suzanne.protobuf")
    fun render(delta: Seconds) {
    model.draw()
    }
    }

    View Slide

  147. open class Content(val filename: String) {
    private var isLoaded: Boolean = false
    private var content: R? = null
    operator fun getValue(thisRef: Any?, property: KProperty<*>): R {
    if (isLoaded) {
    return content!!
    } else {
    throw EarlyAccessException(filename, property.name)
    }
    }
    }

    View Slide

  148. private fun asyncContent(filename: String, enc: (ByteArray) -> T): Content {
    val url = computeUrl(filename)
    val jsonFile = XMLHttpRequest()
    jsonFile.responseType = XMLHttpRequestResponseType.Companion.ARRAYBUFFER
    jsonFile.open("GET", url, true)
    val content = Content(filename)
    jsonFile.onload = { _ ->
    if (jsonFile.readyState == 4.toShort() && jsonFile.status == 200.toShort()) {
    val element = enc((jsonFile.response as ArrayBuffer).toByteArray())
    content.load(element)
    }
    }
    jsonFile.send()
    return content
    }

    View Slide

  149. class DemoSuzanne : Game {
    private val model: Drawable by fileHandler.get("suzanne.protobuf")
    fun render(delta: Seconds) {
    model.draw()
    }
    }
    Content of the wrapper

    Not the wrapper itself

    View Slide

  150. actual fun run(gameFactory: () -> Game) {
    this.game = gameFactory()
    window.requestAnimationFrame(::loading)
    }
    private fun loading(now: Double) {
    if (!fileHandler.isFullyLoaded()) {
    window.requestAnimationFrame(::loading)
    } else {
    game.create()
    game.resume()
    window.requestAnimationFrame(::render)
    }
    }
    Get all resources to load
    Start the game
    Wait for resources to be loaded

    View Slide

  151. {
    "asset": {
    "generator": "Khronos glTF Blender I/O v1.1.46",
    "version": "2.0"
    },
    "nodes": [
    {
    "name": "Light",
    "rotation": [
    0.16907575726509094,
    0.7558803558349609,
    -0.27217137813568115,
    0.570947527885437
    ]
    }
    ],
    "buffers": [
    {
    "byteLength": 26400,
    "uri": "data:application/octet-stream;base64,…"
    }
    ]
    }
    GLTF

    View Slide

  152. JVM
    JS
    ANDROID
    GLTF
    Export
    PROTOBUF
    Conversion
    kotlinx.serialization

    View Slide

  153. @Serializable
    class Mesh(
    @ProtoId(1)
    val vertices: List = emptyList(),
    @ProtoId(2)
    val verticesOrder: IntArray = intArrayOf()
    )
    @Serializable
    class Vertex(
    @ProtoId(1)
    val position: Position,
    @ProtoId(2)
    val color: Color,
    @ProtoId(3)
    val normal: Normal,
    @ProtoId(4)
    val influence: Influence
    )

    View Slide

  154. fun readProtobuf(data: ByteArray): Model {
    val deserializer = ProtoBuf(context = serialModule())
    return deserializer.load(serializer(), data)
    }
    fun writeProtobuf(model: Model): ByteArray {
    val serializer = ProtoBuf(context = serialModule())
    return serializer.dump(Model.serializer(), model)
    }

    View Slide

  155. Joint
    Relative to

    View Slide

  156. View Slide

  157. View Slide

  158. Reference position
    Target position
    Move transformation

    View Slide

  159. HOW TO GET 

    THE MOVE TRANSFORMATION ?

    View Slide

  160. Origin
    A
    B
    OA + AB = OB

    View Slide

  161. Origin
    A
    B
    AB = OB - OA
    Inverse

    View Slide

  162. val animationTransformation = B.globalTransformation * inverse(A.globalTransformation)

    View Slide

  163. uniform mat4 uJointTransformationMatrix[MAX_JOINTS];
    attribute vec3 aVertexPosition;
    attribute int aJointId;
    void main() {
    mat4 uJointMatrix = uJointTransformationMatrix[aJointId];
    gl_Position = uJointMatrix * vec4(aVertexPosition, 1.0);
    }
    All animation moves transformation
    Get transformation for this vertex
    Apply transformation

    View Slide

  164. Try me

    View Slide

  165. View Slide

  166. View Slide

  167. View Slide

  168. KOOL

    View Slide

  169. SO KOTLIN
    MULTIPLATFORM?
    Try me

    View Slide