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(npm("golden-layout", "^1.5.9"))
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(devNpm("file-loader", "^6.0.0"))

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
package world.phantasmal.web.externals.three
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint16Array
import org.w3c.dom.HTMLCanvasElement
@ -123,7 +124,7 @@ external class Quaternion(
/**
* Inverts this quaternion.
*/
fun inverse(): Quaternion
fun invert(): Quaternion
/**
* Multiplies this quaternion by [q].
@ -218,6 +219,7 @@ open external class WebGLRenderer(
override val domElement: HTMLCanvasElement
var autoClearColor: Boolean
var debug: WebGLDebug
override fun render(scene: Object3D, camera: Camera)
@ -232,6 +234,10 @@ open external class WebGLRenderer(
fun dispose()
}
external interface WebGLDebug {
var checkShaderErrors: Boolean
}
open external class Object3D {
/**
* Optional name of the object (doesn't need to be unique).
@ -299,47 +305,25 @@ open external class Object3D {
external class Group : Object3D
open external class Mesh(
geometry: Geometry = definedExternally,
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
) : Object3D {
constructor(
geometry: Geometry,
material: Array<Material>,
)
constructor(
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
)
constructor(
geometry: BufferGeometry,
material: Array<Material>,
)
var geometry: Any /* Geometry | BufferGeometry */
var geometry: BufferGeometry
var material: Any /* Material | Material[] */
fun translateY(distance: Double): Mesh
}
external class SkinnedMesh(
geometry: Geometry = definedExternally,
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
useVertexTexture: Boolean = definedExternally,
) : Mesh {
constructor(
geometry: Geometry,
material: Array<Material>,
useVertexTexture: Boolean = definedExternally,
)
constructor(
geometry: BufferGeometry = definedExternally,
material: Material = definedExternally,
useVertexTexture: Boolean = definedExternally,
)
constructor(
geometry: BufferGeometry,
material: Array<Material>,
@ -352,22 +336,10 @@ external class SkinnedMesh(
}
external class InstancedMesh(
geometry: Geometry,
geometry: BufferGeometry,
material: Material,
count: Int,
) : Mesh {
constructor(
geometry: Geometry,
material: Array<Material>,
count: Int,
)
constructor(
geometry: BufferGeometry,
material: Material,
count: Int,
)
constructor(
geometry: BufferGeometry,
material: Array<Material>,
@ -379,6 +351,7 @@ external class InstancedMesh(
fun getMatrixAt(index: Int, matrix: Matrix4)
fun setMatrixAt(index: Int, matrix: Matrix4)
fun dispose()
}
external class Bone : Object3D {
@ -502,53 +475,6 @@ external class 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 {
var boundingBox: Box3?
var boundingSphere: Sphere?
@ -569,6 +495,13 @@ open external class BufferGeometry : EventDispatcher {
fun dispose()
}
external class PlaneGeometry(
width: Double = definedExternally,
height: Double = definedExternally,
widthSegments: Double = definedExternally,
heightSegments: Double = definedExternally,
) : BufferGeometry
external class CylinderBufferGeometry(
radiusTop: Double = definedExternally,
radiusBottom: Double = definedExternally,
@ -586,6 +519,12 @@ open external class BufferAttribute {
fun copyAt(index1: Int, bufferAttribute: BufferAttribute, index2: Int): BufferAttribute
}
external class Int32BufferAttribute(
array: Int32Array,
itemSize: Int,
normalize: Boolean = definedExternally,
) : BufferAttribute
external class Uint16BufferAttribute(
array: Uint16Array,
itemSize: Int,

View File

@ -1,7 +1,12 @@
package world.phantasmal.web.questEditor.loading
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.jsArrayOf
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.Episode
import world.phantasmal.lib.cursor.cursor
@ -81,8 +86,7 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
private fun addSectionsToCollisionGeometry(collisionGeom: Object3D, renderGeom: Object3D) {
for (collisionArea in collisionGeom.children) {
val origin =
((collisionArea as Mesh).geometry as Geometry).boundingBox!!.getCenter(tmpVec)
val origin = ((collisionArea as Mesh).geometry).boundingBox!!.getCenter(tmpVec)
// Cast a ray downward from the center of the section.
raycaster.set(origin, DOWN)
@ -319,16 +323,14 @@ private fun areaCollisionGeometryToObject3D(
episode: Episode,
areaVariant: AreaVariantModel,
): Object3D {
val obj3d = Group()
obj3d.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}"
val group = Group()
group.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}"
for (collisionMesh in obj.meshes) {
// Use Geometry instead of BufferGeometry for better raycaster performance.
val geom = Geometry()
geom.vertices = Array(collisionMesh.vertices.size) {
vec3ToThree(collisionMesh.vertices[it])
}
val positions = jsArrayOf<Float>()
val normals = jsArrayOf<Float>()
val materialGroups = mutableMapOf<Int, JsArray<Short>>()
var index: Short = 0
for (triangle in collisionMesh.triangles) {
val isSectionTransition = (triangle.flags and 0b1000000) != 0
@ -343,29 +345,47 @@ private fun areaCollisionGeometryToObject3D(
// Filter out walls.
if (materialIndex != 0) {
geom.faces.asDynamic().push(
Face3(
triangle.index1,
triangle.index2,
triangle.index3,
vec3ToThree(triangle.normal),
materialIndex = materialIndex,
)
)
val p1 = collisionMesh.vertices[triangle.index1]
val p2 = collisionMesh.vertices[triangle.index2]
val p3 = collisionMesh.vertices[triangle.index3]
positions.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z)
val n = triangle.normal
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.computeBoundingSphere()
obj3d.add(
group.add(
Mesh(geom, COLLISION_MATERIALS).apply {
renderOrder = 1
}
)
obj3d.add(
group.add(
Mesh(geom, COLLISION_WIREFRAME_MATERIALS).apply {
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 {
q1.setFromEuler(rot)
q2.setFromEuler(section.rotation)
q2.inverse()
q2.invert()
q1 *= q2
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)
// Compute camera position.
val bSphere = (mesh.geometry as BufferGeometry).boundingSphere!!
val bSphere = mesh.geometry.boundingSphere!!
camera.position.copy(cameraPos)
camera.position *= bSphere.radius * cameraDistFactor
camera.lookAt(bSphere.center)
@ -69,7 +69,6 @@ class EntityImageRenderer(
mesh.material = origMaterial
threeRenderer.render(scene, camera)
threeRenderer.render(scene, camera)
return threeRenderer.domElement.toDataURL()
} finally {
// Ensure we dispose the original material and not the background material.

View File

@ -1,5 +1,7 @@
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.questEditor.models.QuestEntityModel
@ -15,13 +17,18 @@ class EntityInstancedMesh(
* [EntityInstancedMesh].
*/
private val modelChanged: (QuestEntityModel<*, *>) -> Unit,
) {
) : TrackedDisposable() {
private val instances: MutableList<EntityInstance> = mutableListOf()
init {
mesh.userData = this
}
override fun dispose() {
disposeObject3DResources(mesh)
super.dispose()
}
fun getInstance(entity: QuestEntityModel<*, *>): EntityInstance? =
instances.find { it.entity == entity }

View File

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

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.widgets
import kotlinx.browser.window
import kotlinx.coroutines.launch
import org.w3c.dom.Node
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.lib.fileFormats.quest.EntityPropType
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.widgets.UnavailableWidget
import world.phantasmal.web.questEditor.controllers.EntityInfoController
import world.phantasmal.web.questEditor.models.QuestEntityPropModel
@ -90,9 +92,21 @@ class EntityInfoWidget(private val ctrl: EntityInfoController) : Widget(enabled
tr {
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(
enabled = ctrl.enabled,
value = value,
value = inputValue,
onChange = onChange,
label = label,
roundTo = 3,

View File

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

View File

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