该源码翻译自 three.js 官方示例 https://github.com/mrdoob/three.js/blob/master/examples/webgl_loader_mmd_audio.html

主要源码如下

import kotlinx.browser.document
import kotlinx.browser.window
import three.*
import kotlin.js.Promise
import kotlin.js.json

external val Math: dynamic
external fun require(module: String): dynamic
external val Ammo: () -> Promise<Unit>

val OutlineEffect: dynamic = require("./jsm/effects/OutlineEffect.js").OutlineEffect
val MMDLoader: dynamic = require("./jsm/loaders/MMDLoader.js").MMDLoader
val MMDAnimationHelper: dynamic = require("./jsm/animation/MMDAnimationHelper.js").MMDAnimationHelper


val jsNew: (classDef: dynamic) -> dynamic = js("function(Clazz){return new Clazz()}")
val jsNew1: (classDef: dynamic, arg: dynamic) -> dynamic = js("function(Clazz,arg){return new Clazz(arg)}")

fun onProgress(xhr: dynamic) {
    if (xhr.lengthComputable) {
        val percentComplete = xhr.loaded / xhr.total * 100
        console.log(Math.round(percentComplete, 2) + "% downloaded")
    }
}

fun init() {
    val overlay = document.getElementById("overlay")
    overlay?.remove();

    val container = document.createElement("div")
    document.body?.appendChild(container)

    var mesh: Mesh
    var ready: Boolean = false
    val clock = Clock()
    val camera = PerspectiveCamera(45.0, window.innerWidth.toDouble() / window.innerHeight, 1.0, 2000.0)

    // Scene
    val scene = Scene()
    scene.background = Color(0xffffff)

    scene.add(PolarGridHelper(30.0, 0.0))

    val listener = AudioListener()
    camera.add(listener)
    scene.add(camera)

    val ambient = AmbientLight(0xaaaaaa, 3f)
    scene.add(ambient)

    val directionalLight = DirectionalLight(0xffffff, 3f)
    directionalLight.position.set(-1f, 1f, 1f).normalize()
    scene.add(directionalLight)

    // renderer
    val renderer = WebGLRenderer(json("antialias" to true))
    renderer.setPixelRatio(window.devicePixelRatio)
    renderer.setSize(window.innerWidth, window.innerHeight)
    container.appendChild(renderer.domElement)

    val effect = jsNew1(OutlineEffect, renderer)

    // model
    val modelFile = "models/mmd/miku/miku_v2.pmd";
    val vmdFiles = arrayOf("models/mmd/vmds/wavefile_v2.vmd")
    val cameraFiles = arrayOf("models/mmd/vmds/wavefile_camera.vmd")
    val audioFile = "models/mmd/audios/wavefile_short.mp3"
    val audioParams = json("delayTime" to 160f * 1 / 30)

    val helper = jsNew(MMDAnimationHelper)

    val loader = jsNew(MMDLoader)

    loader.loadWithAnimation(modelFile, vmdFiles, { mmd ->
        mesh = mmd.mesh;
        helper.add(
            mesh, json(
                "animation" to mmd.animation,
                "physics" to true
            )
        )

        loader.loadAnimation(cameraFiles, camera, { cameraAnimation ->

            helper.add(camera, json("animation" to cameraAnimation))

            AudioLoader().load(audioFile, { buffer ->

                val audio = Audio(listener).setBuffer(buffer);

                helper.add(audio, audioParams);
                scene.add(mesh);

                ready = true;

            }, { onProgress(it) })
        }, { it -> onProgress(it) })
    }, { it -> onProgress(it) })

    window.addEventListener("resize", { _ ->
        camera.aspect = window.innerWidth.toDouble() / window.innerHeight;
        camera.updateProjectionMatrix()

        effect.setSize(window.innerWidth, window.innerHeight);
    })


    // drive
    renderer.setAnimationLoop {
        if (ready) {
            helper.update(clock.getDelta());
        }
        effect.render(scene, camera);
    }
}


fun main() {
    document.querySelector("#startButton")?.addEventListener("click", {
        Ammo().then { init() }
    })
}