Upgraded to three.js 0.126.0 and improved performance of quest renderer.

This commit is contained in:
Daan Vanden Bosch 2021-03-22 20:31:24 +01:00
parent e6a7a5c3ed
commit f57de99577
14 changed files with 160 additions and 139 deletions

View File

@ -42,7 +42,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1")
implementation(npm("golden-layout", "^1.5.9")) implementation(npm("golden-layout", "^1.5.9"))
implementation(npm("monaco-editor", "0.20.0")) implementation(npm("monaco-editor", "0.20.0"))
implementation(npm("three", "^0.122.0")) implementation(npm("three", "^0.126.0"))
implementation(npm("javascript-lp-solver", "0.4.17")) implementation(npm("javascript-lp-solver", "0.4.17"))
implementation(devNpm("file-loader", "^6.0.0")) implementation(devNpm("file-loader", "^6.0.0"))

View File

@ -77,6 +77,7 @@ private fun createThreeRenderer(canvas: HTMLCanvasElement): DisposableThreeRende
}) })
init { init {
renderer.debug.checkShaderErrors = false
renderer.setPixelRatio(window.devicePixelRatio) renderer.setPixelRatio(window.devicePixelRatio)
} }

View File

@ -3,8 +3,8 @@ package world.phantasmal.web.core.rendering
import world.phantasmal.web.externals.three.Object3D import world.phantasmal.web.externals.three.Object3D
/** /**
* Recursively disposes any geometries/materials/textures/skeletons attached to the given [Object3D] * Recursively disposes the given object and any geometries/materials/textures/skeletons attached to
* and its children. * it and its children.
*/ */
fun disposeObject3DResources(obj: Object3D) { fun disposeObject3DResources(obj: Object3D) {
val dynObj = obj.asDynamic() val dynObj = obj.asDynamic()
@ -22,6 +22,10 @@ fun disposeObject3DResources(obj: Object3D) {
dynObj.material.dispose() dynObj.material.dispose()
} }
if (dynObj.dispose != null) {
dynObj.dispose()
}
for (child in obj.children) { for (child in obj.children) {
disposeObject3DResources(child) disposeObject3DResources(child)
} }

View File

@ -125,7 +125,6 @@ class MeshBuilder {
check(positions.size == normals.size) check(positions.size == normals.size)
check(uvs.isEmpty() || positions.size == uvs.size) check(uvs.isEmpty() || positions.size == uvs.size)
// Per-buffer attributes.
val positions = Float32Array(3 * positions.size) val positions = Float32Array(3 * positions.size)
val normals = Float32Array(3 * normals.size) val normals = Float32Array(3 * normals.size)
val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size) val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size)
@ -153,9 +152,6 @@ class MeshBuilder {
geom.setAttribute("normal", Float32BufferAttribute(normals, 3)) geom.setAttribute("normal", Float32BufferAttribute(normals, 3))
uvs?.let { geom.setAttribute("uv", Float32BufferAttribute(uvs, 2)) } uvs?.let { geom.setAttribute("uv", Float32BufferAttribute(uvs, 2)) }
// Per group/material attributes.
val indices = Uint16Array(indexCount)
if (skinning) { if (skinning) {
check(this.positions.size == boneIndices.size / 4) check(this.positions.size == boneIndices.size / 4)
check(this.positions.size == boneWeights.size / 4) check(this.positions.size == boneWeights.size / 4)
@ -174,6 +170,8 @@ class MeshBuilder {
) )
} }
val indices = Uint16Array(indexCount)
var offset = 0 var offset = 0
val texCache = mutableMapOf<Int, Texture?>() val texCache = mutableMapOf<Int, Texture?>()

View File

@ -5,6 +5,7 @@
package world.phantasmal.web.externals.three package world.phantasmal.web.externals.three
import org.khronos.webgl.Float32Array import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint16Array import org.khronos.webgl.Uint16Array
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
@ -123,7 +124,7 @@ external class Quaternion(
/** /**
* Inverts this quaternion. * Inverts this quaternion.
*/ */
fun inverse(): Quaternion fun invert(): Quaternion
/** /**
* Multiplies this quaternion by [q]. * Multiplies this quaternion by [q].
@ -218,6 +219,7 @@ open external class WebGLRenderer(
override val domElement: HTMLCanvasElement override val domElement: HTMLCanvasElement
var autoClearColor: Boolean var autoClearColor: Boolean
var debug: WebGLDebug
override fun render(scene: Object3D, camera: Camera) override fun render(scene: Object3D, camera: Camera)
@ -232,6 +234,10 @@ open external class WebGLRenderer(
fun dispose() fun dispose()
} }
external interface WebGLDebug {
var checkShaderErrors: Boolean
}
open external class Object3D { open external class Object3D {
/** /**
* Optional name of the object (doesn't need to be unique). * Optional name of the object (doesn't need to be unique).
@ -299,47 +305,25 @@ open external class Object3D {
external class Group : Object3D external class Group : Object3D
open external class Mesh( open external class Mesh(
geometry: Geometry = definedExternally, geometry: BufferGeometry = definedExternally,
material: Material = definedExternally, material: Material = definedExternally,
) : Object3D { ) : Object3D {
constructor(
geometry: Geometry,
material: Array<Material>,
)
constructor(
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
)
constructor( constructor(
geometry: BufferGeometry, geometry: BufferGeometry,
material: Array<Material>, material: Array<Material>,
) )
var geometry: Any /* Geometry | BufferGeometry */ var geometry: BufferGeometry
var material: Any /* Material | Material[] */ var material: Any /* Material | Material[] */
fun translateY(distance: Double): Mesh fun translateY(distance: Double): Mesh
} }
external class SkinnedMesh( external class SkinnedMesh(
geometry: Geometry = definedExternally, geometry: BufferGeometry = definedExternally,
material: Material = definedExternally, material: Material = definedExternally,
useVertexTexture: Boolean = definedExternally, useVertexTexture: Boolean = definedExternally,
) : Mesh { ) : Mesh {
constructor(
geometry: Geometry,
material: Array<Material>,
useVertexTexture: Boolean = definedExternally,
)
constructor(
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
useVertexTexture: Boolean = definedExternally,
)
constructor( constructor(
geometry: BufferGeometry, geometry: BufferGeometry,
material: Array<Material>, material: Array<Material>,
@ -352,22 +336,10 @@ external class SkinnedMesh(
} }
external class InstancedMesh( external class InstancedMesh(
geometry: Geometry, geometry: BufferGeometry,
material: Material, material: Material,
count: Int, count: Int,
) : Mesh { ) : Mesh {
constructor(
geometry: Geometry,
material: Array<Material>,
count: Int,
)
constructor(
geometry: BufferGeometry,
material: Material,
count: Int,
)
constructor( constructor(
geometry: BufferGeometry, geometry: BufferGeometry,
material: Array<Material>, material: Array<Material>,
@ -379,6 +351,7 @@ external class InstancedMesh(
fun getMatrixAt(index: Int, matrix: Matrix4) fun getMatrixAt(index: Int, matrix: Matrix4)
fun setMatrixAt(index: Int, matrix: Matrix4) fun setMatrixAt(index: Int, matrix: Matrix4)
fun dispose()
} }
external class Bone : Object3D { external class Bone : Object3D {
@ -502,53 +475,6 @@ external class Color() {
fun setHSL(h: Double, s: Double, l: Double): Color fun setHSL(h: Double, s: Double, l: Double): Color
} }
open external class Geometry : EventDispatcher {
var boundingBox: Box3?
var boundingSphere: Sphere?
/**
* The array of vertices hold every position of points of the model.
* To signal an update in this array, Geometry.verticesNeedUpdate needs to be set to true.
*/
var vertices: Array<Vector3>
/**
* Array of triangles or/and quads.
* The array of faces describe how each vertex in the model is connected with each other.
* To signal an update in this array, Geometry.elementsNeedUpdate needs to be set to true.
*/
var faces: Array<Face3>
/**
* Array of face UV layers.
* Each UV layer is an array of UV matching order and number of vertices in faces.
* To signal an update in this array, Geometry.uvsNeedUpdate needs to be set to true.
*/
var faceVertexUvs: Array<Array<Array<Vector2>>>
fun translate(x: Double, y: Double, z: Double): Geometry
/**
* Computes bounding box of the geometry, updating {@link Geometry.boundingBox} attribute.
*/
fun computeBoundingBox()
/**
* Computes bounding sphere of the geometry, updating Geometry.boundingSphere attribute.
* Neither bounding boxes or bounding spheres are computed by default. They need to be explicitly computed, otherwise they are null.
*/
fun computeBoundingSphere()
fun dispose()
}
external class PlaneGeometry(
width: Double = definedExternally,
height: Double = definedExternally,
widthSegments: Double = definedExternally,
heightSegments: Double = definedExternally,
) : Geometry
open external class BufferGeometry : EventDispatcher { open external class BufferGeometry : EventDispatcher {
var boundingBox: Box3? var boundingBox: Box3?
var boundingSphere: Sphere? var boundingSphere: Sphere?
@ -569,6 +495,13 @@ open external class BufferGeometry : EventDispatcher {
fun dispose() fun dispose()
} }
external class PlaneGeometry(
width: Double = definedExternally,
height: Double = definedExternally,
widthSegments: Double = definedExternally,
heightSegments: Double = definedExternally,
) : BufferGeometry
external class CylinderBufferGeometry( external class CylinderBufferGeometry(
radiusTop: Double = definedExternally, radiusTop: Double = definedExternally,
radiusBottom: Double = definedExternally, radiusBottom: Double = definedExternally,
@ -586,6 +519,12 @@ open external class BufferAttribute {
fun copyAt(index1: Int, bufferAttribute: BufferAttribute, index2: Int): BufferAttribute fun copyAt(index1: Int, bufferAttribute: BufferAttribute, index2: Int): BufferAttribute
} }
external class Int32BufferAttribute(
array: Int32Array,
itemSize: Int,
normalize: Boolean = definedExternally,
) : BufferAttribute
external class Uint16BufferAttribute( external class Uint16BufferAttribute(
array: Uint16Array, array: Uint16Array,
itemSize: Int, itemSize: Int,

View File

@ -1,7 +1,12 @@
package world.phantasmal.web.questEditor.loading package world.phantasmal.web.questEditor.loading
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Uint16Array
import world.phantasmal.core.JsArray
import world.phantasmal.core.asArray
import world.phantasmal.core.asJsArray import world.phantasmal.core.asJsArray
import world.phantasmal.core.jsArrayOf
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.Episode import world.phantasmal.lib.Episode
import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.cursor.cursor
@ -81,8 +86,7 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
private fun addSectionsToCollisionGeometry(collisionGeom: Object3D, renderGeom: Object3D) { private fun addSectionsToCollisionGeometry(collisionGeom: Object3D, renderGeom: Object3D) {
for (collisionArea in collisionGeom.children) { for (collisionArea in collisionGeom.children) {
val origin = val origin = ((collisionArea as Mesh).geometry).boundingBox!!.getCenter(tmpVec)
((collisionArea as Mesh).geometry as Geometry).boundingBox!!.getCenter(tmpVec)
// Cast a ray downward from the center of the section. // Cast a ray downward from the center of the section.
raycaster.set(origin, DOWN) raycaster.set(origin, DOWN)
@ -319,16 +323,14 @@ private fun areaCollisionGeometryToObject3D(
episode: Episode, episode: Episode,
areaVariant: AreaVariantModel, areaVariant: AreaVariantModel,
): Object3D { ): Object3D {
val obj3d = Group() val group = Group()
obj3d.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}" group.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}"
for (collisionMesh in obj.meshes) { for (collisionMesh in obj.meshes) {
// Use Geometry instead of BufferGeometry for better raycaster performance. val positions = jsArrayOf<Float>()
val geom = Geometry() val normals = jsArrayOf<Float>()
val materialGroups = mutableMapOf<Int, JsArray<Short>>()
geom.vertices = Array(collisionMesh.vertices.size) { var index: Short = 0
vec3ToThree(collisionMesh.vertices[it])
}
for (triangle in collisionMesh.triangles) { for (triangle in collisionMesh.triangles) {
val isSectionTransition = (triangle.flags and 0b1000000) != 0 val isSectionTransition = (triangle.flags and 0b1000000) != 0
@ -343,29 +345,47 @@ private fun areaCollisionGeometryToObject3D(
// Filter out walls. // Filter out walls.
if (materialIndex != 0) { if (materialIndex != 0) {
geom.faces.asDynamic().push( val p1 = collisionMesh.vertices[triangle.index1]
Face3( val p2 = collisionMesh.vertices[triangle.index2]
triangle.index1, val p3 = collisionMesh.vertices[triangle.index3]
triangle.index2, positions.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z)
triangle.index3,
vec3ToThree(triangle.normal), val n = triangle.normal
materialIndex = materialIndex, normals.push(n.x, n.y, n.z, n.x, n.y, n.z, n.x, n.y, n.z)
)
) val indices = materialGroups.getOrPut(materialIndex) { jsArrayOf() }
indices.push(index++, index++, index++)
} }
} }
if (geom.faces.isNotEmpty()) { if (index > 0) {
val geom = BufferGeometry()
geom.setAttribute(
"position", Float32BufferAttribute(Float32Array(positions.asArray()), 3),
)
geom.setAttribute(
"normal", Float32BufferAttribute(Float32Array(normals.asArray()), 3),
)
val indices = Uint16Array(index.toInt())
var offset = 0
for ((materialIndex, vertexIndices) in materialGroups) {
indices.set(vertexIndices.asArray(), offset)
geom.addGroup(offset, vertexIndices.length, materialIndex)
offset += vertexIndices.length
}
geom.setIndex(Uint16BufferAttribute(indices, 1))
geom.computeBoundingBox() geom.computeBoundingBox()
geom.computeBoundingSphere() geom.computeBoundingSphere()
obj3d.add( group.add(
Mesh(geom, COLLISION_MATERIALS).apply { Mesh(geom, COLLISION_MATERIALS).apply {
renderOrder = 1 renderOrder = 1
} }
) )
obj3d.add( group.add(
Mesh(geom, COLLISION_WIREFRAME_MATERIALS).apply { Mesh(geom, COLLISION_WIREFRAME_MATERIALS).apply {
renderOrder = 2 renderOrder = 2
} }
@ -373,5 +393,5 @@ private fun areaCollisionGeometryToObject3D(
} }
} }
return obj3d return group
} }

View File

@ -159,7 +159,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
} else { } else {
q1.setFromEuler(rot) q1.setFromEuler(rot)
q2.setFromEuler(section.rotation) q2.setFromEuler(section.rotation)
q2.inverse() q2.invert()
q1 *= q2 q1 *= q2
floorModEuler(q1.toEuler()) floorModEuler(q1.toEuler())
} }

View File

@ -17,5 +17,5 @@ class SectionModel(
} }
} }
val inverseRotation: Euler = rotation.toQuaternion().inverse().toEuler() val inverseRotation: Euler = rotation.toQuaternion().invert().toEuler()
} }

View File

@ -55,7 +55,7 @@ class EntityImageRenderer(
scene.add(light, mesh) scene.add(light, mesh)
// Compute camera position. // Compute camera position.
val bSphere = (mesh.geometry as BufferGeometry).boundingSphere!! val bSphere = mesh.geometry.boundingSphere!!
camera.position.copy(cameraPos) camera.position.copy(cameraPos)
camera.position *= bSphere.radius * cameraDistFactor camera.position *= bSphere.radius * cameraDistFactor
camera.lookAt(bSphere.center) camera.lookAt(bSphere.center)
@ -69,7 +69,6 @@ class EntityImageRenderer(
mesh.material = origMaterial mesh.material = origMaterial
threeRenderer.render(scene, camera) threeRenderer.render(scene, camera)
threeRenderer.render(scene, camera)
return threeRenderer.domElement.toDataURL() return threeRenderer.domElement.toDataURL()
} finally { } finally {
// Ensure we dispose the original material and not the background material. // Ensure we dispose the original material and not the background material.

View File

@ -1,5 +1,7 @@
package world.phantasmal.web.questEditor.rendering package world.phantasmal.web.questEditor.rendering
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.web.core.rendering.disposeObject3DResources
import world.phantasmal.web.externals.three.InstancedMesh import world.phantasmal.web.externals.three.InstancedMesh
import world.phantasmal.web.questEditor.models.QuestEntityModel import world.phantasmal.web.questEditor.models.QuestEntityModel
@ -15,13 +17,18 @@ class EntityInstancedMesh(
* [EntityInstancedMesh]. * [EntityInstancedMesh].
*/ */
private val modelChanged: (QuestEntityModel<*, *>) -> Unit, private val modelChanged: (QuestEntityModel<*, *>) -> Unit,
) { ) : TrackedDisposable() {
private val instances: MutableList<EntityInstance> = mutableListOf() private val instances: MutableList<EntityInstance> = mutableListOf()
init { init {
mesh.userData = this mesh.userData = this
} }
override fun dispose() {
disposeObject3DResources(mesh)
super.dispose()
}
fun getInstance(entity: QuestEntityModel<*, *>): EntityInstance? = fun getInstance(entity: QuestEntityModel<*, *>): EntityInstance? =
instances.find { it.entity == entity } instances.find { it.entity == entity }

View File

@ -36,7 +36,7 @@ class EntityMeshManager(
add(entity) add(entity)
}) })
}, },
{ /* Nothing to dispose. */ }, EntityInstancedMesh::dispose,
) )
) )

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.widgets package world.phantasmal.web.questEditor.widgets
import kotlinx.browser.window
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.w3c.dom.Node import org.w3c.dom.Node
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
@ -8,6 +9,7 @@ import world.phantasmal.core.math.degToRad
import world.phantasmal.core.math.radToDeg import world.phantasmal.core.math.radToDeg
import world.phantasmal.lib.fileFormats.quest.EntityPropType import world.phantasmal.lib.fileFormats.quest.EntityPropType
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.widgets.UnavailableWidget import world.phantasmal.web.core.widgets.UnavailableWidget
import world.phantasmal.web.questEditor.controllers.EntityInfoController import world.phantasmal.web.questEditor.controllers.EntityInfoController
import world.phantasmal.web.questEditor.models.QuestEntityPropModel import world.phantasmal.web.questEditor.models.QuestEntityPropModel
@ -90,9 +92,21 @@ class EntityInfoWidget(private val ctrl: EntityInfoController) : Widget(enabled
tr { tr {
className = COORD_CLASS className = COORD_CLASS
val inputValue = mutableVal(value.value)
var timeout = -1
observe(value) {
if (timeout == -1) {
timeout = window.setTimeout({
inputValue.value = value.value
timeout = -1
})
}
}
val input = DoubleInput( val input = DoubleInput(
enabled = ctrl.enabled, enabled = ctrl.enabled,
value = value, value = inputValue,
onChange = onChange, onChange = onChange,
label = label, label = label,
roundTo = 3, roundTo = 3,

View File

@ -103,8 +103,7 @@ class MeshRenderer(
if (resetCamera) { if (resetCamera) {
// Compute camera position. // Compute camera position.
val geom = mesh.geometry as BufferGeometry val bSphere = mesh.geometry.boundingSphere!!
val bSphere = geom.boundingSphere!!
val cameraDistFactor = val cameraDistFactor =
1.5 / tan(degToRad((context.camera as PerspectiveCamera).fov) / 2) 1.5 / tan(degToRad((context.camera as PerspectiveCamera).fov) / 2)
val cameraPos = CAMERA_POS * (bSphere.radius * cameraDistFactor) val cameraPos = CAMERA_POS * (bSphere.radius * cameraDistFactor)

View File

@ -1,6 +1,8 @@
package world.phantasmal.web.viewer.rendering package world.phantasmal.web.viewer.rendering
import mu.KotlinLogging import mu.KotlinLogging
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Uint16Array
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
import world.phantasmal.lib.fileFormats.ninja.XvrTexture import world.phantasmal.lib.fileFormats.ninja.XvrTexture
import world.phantasmal.web.core.rendering.* import world.phantasmal.web.core.rendering.*
@ -51,8 +53,8 @@ class TextureRenderer(
private fun texturesChanged(textures: List<XvrTexture>) { private fun texturesChanged(textures: List<XvrTexture>) {
meshes.forEach { mesh -> meshes.forEach { mesh ->
disposeObject3DResources(mesh)
context.scene.remove(mesh) context.scene.remove(mesh)
disposeObject3DResources(mesh)
} }
inputManager.resetCamera() inputManager.resetCamera()
@ -109,21 +111,59 @@ class TextureRenderer(
} }
} }
private fun createQuad(x: Int, y: Int, width: Int, height: Int): PlaneGeometry { private fun createQuad(x: Int, y: Int, width: Int, height: Int): BufferGeometry {
val quad = PlaneGeometry( val halfWidth = width / 2f
width.toDouble(), val halfHeight = height / 2f
height.toDouble(),
widthSegments = 1.0, val geom = BufferGeometry()
heightSegments = 1.0,
geom.setAttribute(
"position",
Float32BufferAttribute(
Float32Array(arrayOf(
-halfWidth, -halfHeight, 0f,
-halfWidth, halfHeight, 0f,
halfWidth, halfHeight, 0f,
halfWidth, -halfHeight, 0f,
)),
3,
),
) )
quad.faceVertexUvs = arrayOf( geom.setAttribute(
arrayOf( "normal",
arrayOf(Vector2(0.0, 0.0), Vector2(0.0, 1.0), Vector2(1.0, 0.0)), Float32BufferAttribute(
arrayOf(Vector2(0.0, 1.0), Vector2(1.0, 1.0), Vector2(1.0, 0.0)), Float32Array(arrayOf(
) 0f, 0f, 1f,
0f, 0f, 1f,
0f, 0f, 1f,
0f, 0f, 1f,
)),
3,
),
) )
quad.translate(x.toDouble(), y.toDouble(), -5.0) geom.setAttribute(
return quad "uv",
Float32BufferAttribute(
Float32Array(arrayOf(
0f, 1f,
0f, 0f,
1f, 0f,
1f, 1f,
)),
2,
),
)
geom.setIndex(Uint16BufferAttribute(
Uint16Array(arrayOf(
0, 2, 1,
2, 0, 3,
)),
1,
))
geom.translate(x.toDouble(), y.toDouble(), -5.0)
return geom
} }
companion object { companion object {