From 29192e5684ef110d32c2b75990c7360834e13835 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 11 Apr 2021 21:43:34 +0200 Subject: [PATCH] Added manual area render geometry culling code. --- .../phantasmal/core/PrimitiveExtensions.kt | 9 + .../jsMain/kotlin/world/phantasmal/core/Js.kt | 14 +- .../phantasmal/core/PrimitiveExtensions.kt | 5 + .../phantasmal/core/PrimitiveExtensions.kt | 2 + .../lib/fileFormats/AreaRenderGeometry.kt | 184 +++++++--- .../lib/fileFormats/ninja/Motion.kt | 4 +- .../phantasmal/lib/fileFormats/ninja/Ninja.kt | 27 +- .../lib/fileFormats/ninja/NinjaObject.kt | 73 +++- .../conversion/NinjaGeometryConversion.kt | 102 ++++-- .../questEditor/loading/AreaAssetLoader.kt | 327 +++++++++++++----- .../web/viewer/stores/ViewerStore.kt | 4 +- 11 files changed, 551 insertions(+), 200 deletions(-) diff --git a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index bd63978b..91d07ae1 100644 --- a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -20,6 +20,15 @@ fun Int.isBitSet(bit: Int): Boolean = fun UByte.isBitSet(bit: Int): Boolean = toInt().isBitSet(bit) +fun Int.setBit(bit: Int, value: Boolean): Int = + if (value) { + this or (1 shl bit) + } else { + this and (1 shl bit).inv() + } + expect fun Int.reinterpretAsFloat(): Float expect fun Float.reinterpretAsInt(): Int + +expect fun Float.reinterpretAsUInt(): UInt diff --git a/core/src/jsMain/kotlin/world/phantasmal/core/Js.kt b/core/src/jsMain/kotlin/world/phantasmal/core/Js.kt index d464961c..425bdfff 100644 --- a/core/src/jsMain/kotlin/world/phantasmal/core/Js.kt +++ b/core/src/jsMain/kotlin/world/phantasmal/core/Js.kt @@ -43,9 +43,9 @@ inline val JsPair<*, T>.second: T get() = asDynamic()[1].unsafeCast() inline operator fun JsPair.component1(): T = first inline operator fun JsPair<*, T>.component2(): T = second -@Suppress("UNUSED_PARAMETER") -inline fun objectKeys(jsObject: dynamic): Array = - js("Object.keys(jsObject)").unsafeCast>() +@Suppress("FunctionName", "UNUSED_PARAMETER") +inline fun JsPair(first: A, second: B): JsPair = + js("[first, second]").unsafeCast>() @Suppress("UNUSED_PARAMETER") inline fun objectEntries(jsObject: dynamic): Array> = @@ -64,6 +64,10 @@ external interface JsSet { inline fun emptyJsSet(): JsSet = js("new Set()").unsafeCast>() +@Suppress("UNUSED_PARAMETER") +inline fun jsSetOf(vararg values: T): JsSet = + js("new Set(values)").unsafeCast>() + external interface JsMap { val size: Int @@ -75,5 +79,9 @@ external interface JsMap { fun set(key: K, value: V): JsMap } +@Suppress("UNUSED_PARAMETER") +inline fun jsMapOf(vararg pairs: JsPair): JsMap = + js("new Map(pairs)").unsafeCast>() + inline fun emptyJsMap(): JsMap = js("new Map()").unsafeCast>() diff --git a/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index 84b67f0b..89ed8dc3 100644 --- a/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -14,3 +14,8 @@ actual fun Float.reinterpretAsInt(): Int { dataView.setFloat32(0, this) return dataView.getInt32(0) } + +actual fun Float.reinterpretAsUInt(): UInt { + dataView.setFloat32(0, this) + return dataView.getUint32(0).toUInt() +} diff --git a/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index 0d811ecb..538e7110 100644 --- a/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -8,3 +8,5 @@ import java.lang.Float.intBitsToFloat actual fun Int.reinterpretAsFloat(): Float = intBitsToFloat(this) actual fun Float.reinterpretAsInt(): Int = floatToIntBits(this) + +actual fun Float.reinterpretAsUInt(): UInt = reinterpretAsInt().toUInt() diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt index 2f824755..9f6f99f5 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt @@ -1,40 +1,69 @@ package world.phantasmal.lib.fileFormats +import mu.KotlinLogging import world.phantasmal.core.isBitSet import world.phantasmal.lib.cursor.Cursor -import world.phantasmal.lib.fileFormats.ninja.XjObject -import world.phantasmal.lib.fileFormats.ninja.angleToRad -import world.phantasmal.lib.fileFormats.ninja.parseXjObject +import world.phantasmal.lib.fileFormats.ninja.* -class RenderGeometry( - val sections: List, +private val logger = KotlinLogging.logger {} + +class AreaGeometry( + val sections: List, ) -class RenderSection( +class AreaSection( val id: Int, val position: Vec3, val rotation: Vec3, - val objects: List, - val animatedObjects: List, + val radius: Float, + val objects: List, + val animatedObjects: List, ) -fun parseAreaRenderGeometry(cursor: Cursor): RenderGeometry { - val sections = mutableListOf() +sealed class AreaObject { + abstract val offset: Int + abstract val xjObject: XjObject + abstract val flags: Int - cursor.seekEnd(16) + class Simple( + override val offset: Int, + override val xjObject: XjObject, + override val flags: Int, + ) : AreaObject() + class Animated( + override val offset: Int, + override val xjObject: XjObject, + val njMotion: NjMotion, + val speed: Float, + override val flags: Int, + ) : AreaObject() +} + +fun parseAreaRenderGeometry(cursor: Cursor): AreaGeometry { val dataOffset = parseRel(cursor, parseIndex = false).dataOffset + cursor.seekStart(dataOffset) - cursor.seek(8) // Format "fmt2" in UTF-16. - val sectionCount = cursor.int() + val format = cursor.stringAscii(maxByteLength = 4, nullTerminated = true, dropRemaining = true) + + if (format != "fmt2") { + logger.warn { """Expected format to be "fmt2" but was "$format".""" } + } + cursor.seek(4) - val sectionTableOffset = cursor.int() - // val textureNameOffset = cursor.int() + val sectionsCount = cursor.int() + cursor.seek(4) + val sectionsOffset = cursor.int() - val xjObjectCache = mutableMapOf>() + val sections = mutableListOf() - for (i in 0 until sectionCount) { - cursor.seekStart(sectionTableOffset + 52 * i) + // Cache keys are offsets. + val simpleAreaObjectCache = mutableMapOf>() + val animatedAreaObjectCache = mutableMapOf>() + val njMotionCache = mutableMapOf() + + for (i in 0 until sectionsCount) { + cursor.seekStart(sectionsOffset + 52 * i) val sectionId = cursor.int() val sectionPosition = cursor.vec3Float() @@ -44,67 +73,110 @@ fun parseAreaRenderGeometry(cursor: Cursor): RenderGeometry { angleToRad(cursor.int()), ) - cursor.seek(4) // Radius? + val radius = cursor.float() - val simpleGeometryOffsetTableOffset = cursor.int() - val animatedGeometryOffsetTableOffset = cursor.int() - val simpleGeometryOffsetCount = cursor.int() - val animatedGeometryOffsetCount = cursor.int() + val simpleAreaObjectsOffset = cursor.int() + val animatedAreaObjectsOffset = cursor.int() + val simpleAreaObjectsCount = cursor.int() + val animatedAreaObjectsCount = cursor.int() // Ignore the last 4 bytes. - val objects = parseGeometryTable( - cursor, - xjObjectCache, - simpleGeometryOffsetTableOffset, - simpleGeometryOffsetCount, - animated = false, - ) +// println("section $sectionId (index $i), simple geom at $simpleGeometryTableOffset, animated geom at $animatedGeometryTableOffset") - val animatedObjects = parseGeometryTable( - cursor, - xjObjectCache, - animatedGeometryOffsetTableOffset, - animatedGeometryOffsetCount, - animated = true, - ) + @Suppress("UNCHECKED_CAST") + val simpleObjects = simpleAreaObjectCache.getOrPut(simpleAreaObjectsOffset) { + parseAreaObjects( + cursor, + njMotionCache, + simpleAreaObjectsOffset, + simpleAreaObjectsCount, + animated = false, + ) as List + } - sections.add(RenderSection( + @Suppress("UNCHECKED_CAST") + val animatedObjects = animatedAreaObjectCache.getOrPut(animatedAreaObjectsOffset) { + parseAreaObjects( + cursor, + njMotionCache, + animatedAreaObjectsOffset, + animatedAreaObjectsCount, + animated = true, + ) as List + } + + sections.add(AreaSection( sectionId, sectionPosition, sectionRotation, - objects, + radius, + simpleObjects, animatedObjects, )) } - return RenderGeometry(sections) + return AreaGeometry(sections) } -private fun parseGeometryTable( +private fun parseAreaObjects( cursor: Cursor, - xjObjectCache: MutableMap>, - tableOffset: Int, - tableEntryCount: Int, + njMotionCache: MutableMap, + offset: Int, + count: Int, animated: Boolean, -): List { - val tableEntrySize = if (animated) 32 else 16 - val objects = mutableListOf() +): List { + val objectSize = if (animated) 32 else 16 + val objects = mutableListOf() - for (i in 0 until tableEntryCount) { - cursor.seekStart(tableOffset + tableEntrySize * i) + for (i in 0 until count) { + val objectOffset = offset + objectSize * i + cursor.seekStart(objectOffset) + + var xjObjectOffset = cursor.int() + val speed: Float? + val njMotionOffset: Int? + + if (animated) { + njMotionOffset = cursor.int() + cursor.seek(8) + speed = cursor.float() + } else { + speed = null + njMotionOffset = null + } + + val slideTextureIdOffset = cursor.int() + val swapTextureIdOffset = cursor.int() - var offset = cursor.int() - cursor.seek(8) val flags = cursor.int() if (flags.isBitSet(2)) { - offset = cursor.seekStart(offset).int() + xjObjectOffset = cursor.seekStart(xjObjectOffset).int() } - objects.addAll( - xjObjectCache.getOrPut(offset) { - cursor.seekStart(offset) - parseXjObject(cursor) + cursor.seekStart(xjObjectOffset) + val xjObjects = parseXjObject(cursor) + + if (xjObjects.size > 1) { + logger.warn { + "Expected exactly one xjObject at ${xjObjectOffset}, got ${xjObjects.size}." + } + } + + val xjObject = xjObjects.first() + + val njMotion = njMotionOffset?.let { + njMotionCache.getOrPut(njMotionOffset) { + cursor.seekStart(njMotionOffset) + parseMotion(cursor, v2Format = false) + } + } + + objects.add( + if (animated) { + AreaObject.Animated(objectOffset, xjObject, njMotion!!, speed!!, flags) + } else { + AreaObject.Simple(objectOffset, xjObject, flags) } ) } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Motion.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Motion.kt index 4db766c6..9d63f28a 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Motion.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Motion.kt @@ -101,13 +101,13 @@ private fun parseAction(cursor: Cursor): NjMotion { return parseMotion(cursor, v2Format = false) } -private fun parseMotion(cursor: Cursor, v2Format: Boolean): NjMotion { +fun parseMotion(cursor: Cursor, v2Format: Boolean): NjMotion { // For v2, try to determine the end of the mData offset table by finding the lowest mDataOffset // value. This is usually the value that the first mDataOffset points to. This value is assumed // to be the end of the mDataOffset table. var mDataTableEnd = if (v2Format) cursor.size else cursor.position - // Points to an array the size of boneCount. + // Points to an array with an element per bone. val mDataTableOffset = cursor.int() val frameCount = cursor.int() val type = cursor.uShort().toInt() diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Ninja.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Ninja.kt index 399aa610..578f823e 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Ninja.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Ninja.kt @@ -3,7 +3,6 @@ package world.phantasmal.lib.fileFormats.ninja import world.phantasmal.core.Failure import world.phantasmal.core.PwResult import world.phantasmal.core.Success -import world.phantasmal.core.isBitSet import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.fileFormats.Vec3 import world.phantasmal.lib.fileFormats.parseIff @@ -21,6 +20,7 @@ fun parseXjObject(cursor: Cursor): List = parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, ::XjObject, Unit) private typealias CreateObject = ( + offset: Int, evaluationFlags: NinjaEvaluationFlags, model: Model?, position: Vec3, @@ -57,17 +57,8 @@ private fun , Context> parseSi createObject: CreateObject, context: Context, ): MutableList { + val offset = cursor.position val evalFlags = cursor.int() - val noTranslate = evalFlags.isBitSet(0) - val noRotate = evalFlags.isBitSet(1) - val noScale = evalFlags.isBitSet(2) - val hidden = evalFlags.isBitSet(3) - val breakChildTrace = evalFlags.isBitSet(4) - val zxyRotationOrder = evalFlags.isBitSet(5) - val skip = evalFlags.isBitSet(6) - val shapeSkip = evalFlags.isBitSet(7) - val clip = evalFlags.isBitSet(8) - val modifier = evalFlags.isBitSet(9) val modelOffset = cursor.int() val pos = cursor.vec3Float() @@ -102,18 +93,8 @@ private fun , Context> parseSi } val obj = createObject( - NinjaEvaluationFlags( - noTranslate, - noRotate, - noScale, - hidden, - breakChildTrace, - zxyRotationOrder, - skip, - shapeSkip, - clip, - modifier, - ), + offset, + NinjaEvaluationFlags(evalFlags), model, pos, rotation, diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/NinjaObject.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/NinjaObject.kt index b323edba..9706c7af 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/NinjaObject.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/NinjaObject.kt @@ -1,9 +1,12 @@ package world.phantasmal.lib.fileFormats.ninja +import world.phantasmal.core.isBitSet +import world.phantasmal.core.setBit import world.phantasmal.lib.fileFormats.Vec2 import world.phantasmal.lib.fileFormats.Vec3 sealed class NinjaObject>( + val offset: Int, val evaluationFlags: NinjaEvaluationFlags, val model: Model?, val position: Vec3, @@ -57,6 +60,7 @@ sealed class NinjaObject>( } class NjObject( + offset: Int, evaluationFlags: NinjaEvaluationFlags, model: NjModel?, position: Vec3, @@ -64,6 +68,7 @@ class NjObject( scale: Vec3, children: MutableList, ) : NinjaObject( + offset, evaluationFlags, model, position, @@ -73,6 +78,7 @@ class NjObject( ) class XjObject( + offset: Int, evaluationFlags: NinjaEvaluationFlags, model: XjModel?, position: Vec3, @@ -80,6 +86,7 @@ class XjObject( scale: Vec3, children: MutableList, ) : NinjaObject( + offset, evaluationFlags, model, position, @@ -88,18 +95,60 @@ class XjObject( children, ) -class NinjaEvaluationFlags( - var noTranslate: Boolean, - var noRotate: Boolean, - var noScale: Boolean, - var hidden: Boolean, - var breakChildTrace: Boolean, - var zxyRotationOrder: Boolean, - var skip: Boolean, - var shapeSkip: Boolean, - val clip: Boolean, - val modifier: Boolean, -) +class NinjaEvaluationFlags(bits: Int) { + var bits: Int = bits + private set + var noTranslate: Boolean + get() = bits.isBitSet(0) + set(value) { + bits = bits.setBit(0, value) + } + var noRotate: Boolean + get() = bits.isBitSet(1) + set(value) { + bits = bits.setBit(1, value) + } + var noScale: Boolean + get() = bits.isBitSet(2) + set(value) { + bits = bits.setBit(2, value) + } + var hidden: Boolean + get() = bits.isBitSet(3) + set(value) { + bits = bits.setBit(3, value) + } + var breakChildTrace: Boolean + get() = bits.isBitSet(4) + set(value) { + bits = bits.setBit(4, value) + } + var zxyRotationOrder: Boolean + get() = bits.isBitSet(5) + set(value) { + bits = bits.setBit(5, value) + } + var skip: Boolean + get() = bits.isBitSet(6) + set(value) { + bits = bits.setBit(6, value) + } + var shapeSkip: Boolean + get() = bits.isBitSet(7) + set(value) { + bits = bits.setBit(7, value) + } + var clip: Boolean + get() = bits.isBitSet(8) + set(value) { + bits = bits.setBit(8, value) + } + var modifier: Boolean + get() = bits.isBitSet(9) + set(value) { + bits = bits.setBit(9, value) + } +} sealed class NinjaModel diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt index 08d740f2..04f76c30 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt @@ -4,10 +4,7 @@ import mu.KotlinLogging import org.khronos.webgl.Float32Array import org.khronos.webgl.Uint16Array import world.phantasmal.core.* -import world.phantasmal.lib.fileFormats.CollisionGeometry -import world.phantasmal.lib.fileFormats.CollisionTriangle -import world.phantasmal.lib.fileFormats.RenderGeometry -import world.phantasmal.lib.fileFormats.RenderSection +import world.phantasmal.lib.fileFormats.* import world.phantasmal.lib.fileFormats.ninja.* import world.phantasmal.web.core.dot import world.phantasmal.web.core.toQuaternion @@ -76,6 +73,11 @@ private val tmpNormal = Vector3() private val tmpVec = Vector3() private val tmpNormalMatrix = Matrix3() +interface AreaObjectUserData { + var sectionId: Int + var areaObject: AreaObject +} + fun ninjaObjectToMesh( ninjaObject: NinjaObject<*, *>, textures: List, @@ -121,24 +123,36 @@ fun ninjaObjectToMeshBuilder( } fun renderGeometryToGroup( - renderGeometry: RenderGeometry, + renderGeometry: AreaGeometry, textures: List, - processMesh: (RenderSection, XjObject, Mesh) -> Unit = { _, _, _ -> }, + processMesh: (AreaSection, AreaObject, Mesh) -> Unit = { _, _, _ -> }, ): Group { val group = Group() val textureCache = emptyJsMap() val meshCache = emptyJsMap() - for ((i, section) in renderGeometry.sections.withIndex()) { - for (xjObj in section.objects) { - group.add(xjObjectToMesh( - textures, textureCache, meshCache, xjObj, i, section, processMesh, + for ((sectionIndex, section) in renderGeometry.sections.withIndex()) { + for (areaObj in section.objects) { + group.add(areaObjectToMesh( + textures, + textureCache, + meshCache, + section, + sectionIndex, + areaObj, + processMesh, )) } - for (xjObj in section.animatedObjects) { - group.add(xjObjectToMesh( - textures, textureCache, meshCache, xjObj, i, section, processMesh, + for (areaObj in section.animatedObjects) { + group.add(areaObjectToMesh( + textures, + textureCache, + meshCache, + section, + sectionIndex, + areaObj, + processMesh, )) } } @@ -146,41 +160,83 @@ fun renderGeometryToGroup( return group } -private fun xjObjectToMesh( +/** + * Calculates a fingerprint that can be used to match duplicated [AreaObject]s across sections, area + * variants and even areas. + */ +fun AreaObject.fingerPrint(): String = + buildString { + append(if (this@fingerPrint is AreaObject.Animated) 'a' else 's') + + append('_') + + var evalFlags = 0 + var childCount = 0 + var vertCount = 0 + var meshCount = 0 + var radius = 0f + + fun recurse(xjObject: XjObject) { + evalFlags = evalFlags or xjObject.evaluationFlags.bits + childCount += xjObject.children.size + vertCount += xjObject.model?.vertices?.size ?: 0 + meshCount += xjObject.model?.meshes?.size ?: 0 + radius += xjObject.model?.collisionSphereRadius ?: 0f + xjObject.children.forEach(::recurse) + } + + recurse(xjObject) + + append(evalFlags.toString(36)) + append('_') + append(childCount.toString(36)) + append('_') + append(vertCount.toString(36)) + append('_') + append(meshCount.toString(36)) + append('_') + append(radius.reinterpretAsUInt().toString(36)) + } + +private fun areaObjectToMesh( textures: List, textureCache: JsMap, meshCache: JsMap, - xjObj: XjObject, - index: Int, - section: RenderSection, - processMesh: (RenderSection, XjObject, Mesh) -> Unit, + section: AreaSection, + sectionIndex: Int, + areaObj: AreaObject, + processMesh: (AreaSection, AreaObject, Mesh) -> Unit, ): Mesh { - var mesh = meshCache.get(xjObj) + var mesh = meshCache.get(areaObj.xjObject) if (mesh == null) { val builder = MeshBuilder(textures, textureCache) - ninjaObjectToMeshBuilder(xjObj, builder) + ninjaObjectToMeshBuilder(areaObj.xjObject, builder) builder.defaultMaterial(MeshLambertMaterial(obj { - color = Color().setHSL((index % 7) / 7.0, 1.0, .5) + color = Color().setHSL((sectionIndex % 7) / 7.0, 1.0, .5) transparent = true opacity = .5 side = DoubleSide })) mesh = builder.buildMesh(boundingVolumes = true) - meshCache.set(xjObj, mesh) + meshCache.set(areaObj.xjObject, mesh) } else { // If we already have a mesh for this XjObject, make a copy and reuse the existing buffer // geometry and materials. mesh = Mesh(mesh.geometry, mesh.material.unsafeCast>()) } + val userData = mesh.userData.unsafeCast() + userData.sectionId = section.id + userData.areaObject = areaObj + mesh.position.setFromVec3(section.position) mesh.rotation.setFromVec3(section.rotation) mesh.updateMatrixWorld() - processMesh(section, xjObj, mesh) + processMesh(section, areaObj, mesh) return mesh } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt index e353623d..b9b16617 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt @@ -1,18 +1,14 @@ package world.phantasmal.web.questEditor.loading import org.khronos.webgl.ArrayBuffer -import world.phantasmal.core.asJsArray -import world.phantasmal.core.isBitSet +import world.phantasmal.core.* import world.phantasmal.lib.Endianness import world.phantasmal.lib.Episode import world.phantasmal.lib.cursor.cursor -import world.phantasmal.lib.fileFormats.CollisionGeometry -import world.phantasmal.lib.fileFormats.RenderGeometry +import world.phantasmal.lib.fileFormats.* import world.phantasmal.lib.fileFormats.ninja.XjObject import world.phantasmal.lib.fileFormats.ninja.XvrTexture import world.phantasmal.lib.fileFormats.ninja.parseXvm -import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry -import world.phantasmal.lib.fileFormats.parseAreaRenderGeometry import world.phantasmal.web.core.dot import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.rendering.conversion.* @@ -123,44 +119,44 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } } - private fun cullRenderGeometry(collisionGeom: Object3D, renderGeom: Object3D) { - val cullingVolumes = mutableMapOf() - - for (collisionMesh in collisionGeom.children) { - collisionMesh as Mesh - collisionMesh.userData.unsafeCast().section?.let { section -> - cullingVolumes.getOrPut(section.id, ::Box3) - .union( - collisionMesh.geometry.boundingBox!!.applyMatrix4(collisionMesh.matrixWorld) - ) - } - } - - for (cullingVolume in cullingVolumes.values) { - cullingVolume.min.x -= 50 - cullingVolume.min.y = cullingVolume.max.y + 20 - cullingVolume.min.z -= 50 - cullingVolume.max.x += 50 - cullingVolume.max.y = Double.POSITIVE_INFINITY - cullingVolume.max.z += 50 - } - - var i = 0 - - outer@ while (i < renderGeom.children.size) { - val renderMesh = renderGeom.children[i] as Mesh - val bb = renderMesh.geometry.boundingBox!!.applyMatrix4(renderMesh.matrixWorld) - - for (cullingVolume in cullingVolumes.values) { - if (bb.intersectsBox(cullingVolume)) { - renderGeom.remove(renderMesh) - continue@outer - } - } - - i++ - } - } +// private fun cullRenderGeometry(collisionGeom: Object3D, renderGeom: Object3D) { +// val cullingVolumes = mutableMapOf() +// +// for (collisionMesh in collisionGeom.children) { +// collisionMesh as Mesh +// collisionMesh.userData.unsafeCast().section?.let { section -> +// cullingVolumes.getOrPut(section.id, ::Box3) +// .union( +// collisionMesh.geometry.boundingBox!!.applyMatrix4(collisionMesh.matrixWorld) +// ) +// } +// } +// +// for (cullingVolume in cullingVolumes.values) { +// cullingVolume.min.x -= 50 +// cullingVolume.min.y = cullingVolume.max.y + 20 +// cullingVolume.min.z -= 50 +// cullingVolume.max.x += 50 +// cullingVolume.max.y = Double.POSITIVE_INFINITY +// cullingVolume.max.z += 50 +// } +// +// var i = 0 +// +// outer@ while (i < renderGeom.children.size) { +// val renderMesh = renderGeom.children[i] as Mesh +// val bb = renderMesh.geometry.boundingBox!!.applyMatrix4(renderMesh.matrixWorld) +// +// for (cullingVolume in cullingVolumes.values) { +// if (bb.intersectsBox(cullingVolume)) { +// renderGeom.remove(renderMesh) +// continue@outer +// } +// } +// +// i++ +// } +// } private fun areaAssetUrl( episode: Episode, @@ -206,18 +202,25 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } private fun areaGeometryToObject3DAndSections( - renderGeometry: RenderGeometry, + renderGeometry: AreaGeometry, textures: List, episode: Episode, areaVariant: AreaVariantModel, ): Pair> { - val renderOnTopTextures = RENDER_ON_TOP_TEXTURES[Pair(episode, areaVariant.area.id)] + val fix = MANUAL_FIXES[Pair(episode, areaVariant.area.id)] val sections = mutableMapOf() + console.log(renderGeometry) val group = - renderGeometryToGroup(renderGeometry, textures) { renderSection, xjObject, mesh -> - if (shouldRenderOnTop(xjObject, renderOnTopTextures)) { - mesh.renderOrder = 1 + renderGeometryToGroup(renderGeometry, textures) { renderSection, areaObj, mesh -> + if (fix != null) { + if (fix.shouldRenderOnTop(areaObj.xjObject)) { + mesh.renderOrder = 1 + } + + if (fix.shouldHide(areaObj)) { + mesh.visible = false + } } if (renderSection.id >= 0) { @@ -237,24 +240,6 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine return Pair(group, sections.values.toList()) } - private fun shouldRenderOnTop(obj: XjObject, renderOnTopTextures: Set?): Boolean { - renderOnTopTextures ?: return false - - obj.model?.meshes?.let { meshes -> - for (mesh in meshes) { - mesh.material.textureId?.let { textureId -> - if (textureId in renderOnTopTextures) { - return true - } - } - } - } - - return obj.children.any { - shouldRenderOnTop(it, renderOnTopTextures) - } - } - private fun areaCollisionGeometryToObject3D( obj: CollisionGeometry, episode: Episode, @@ -288,6 +273,38 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine Render, Collision, Texture } + private class Fix( + /** + * Textures that should be rendered on top of other textures. These are usually very + * translucent. E.g. forest 1 has a mesh with baked-in shadow that's overlaid over the + * regular geometry. Might not be necessary anymore once order-independent rendering is + * implemented. + */ + private val renderOnTopTextures: JsSet = emptyJsSet(), + /** + * Set of [AreaObject] finger prints. + * These objects should be hidden because they cover floors and other useful geometry. + */ + private val hiddenObjects: JsSet = emptyJsSet(), + ) { + fun shouldRenderOnTop(obj: XjObject): Boolean { + obj.model?.meshes?.let { meshes -> + for (mesh in meshes) { + mesh.material.textureId?.let { textureId -> + if (renderOnTopTextures.has(textureId)) { + return true + } + } + } + } + + return obj.children.any(::shouldRenderOnTop) + } + + fun shouldHide(areaObj: AreaObject): Boolean = + hiddenObjects.has(areaObj.fingerPrint()) + } + companion object { private val COS_75_DEG = cos(PI / 180 * 75) private val DOWN = Vector3(.0, -1.0, .0) @@ -346,26 +363,178 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine ) /** - * Mapping of episode and area ID to set of texture IDs. - * Manual fixes for various areas. Might not be necessary anymore once order-independent - * rendering is implemented. + * Mapping of episode and area ID to data for manually fixing issues with the render + * geometry. */ - val RENDER_ON_TOP_TEXTURES: Map, Set> = mapOf( + private val MANUAL_FIXES: Map, Fix> = mutableMapOf( // Pioneer 2 - Pair(Episode.I, 0) to setOf( - 70, 71, 72, 126, 127, 155, 156, 198, 230, 231, 232, 233, 234, + Pair(Episode.I, 0) to Fix( + renderOnTopTextures = jsSetOf( + 70, 71, 72, 126, 127, 155, 156, 198, 230, 231, 232, 233, 234, + ), + hiddenObjects = jsSetOf( + "s_m_0_6a_d_iu5sg6", + "s_m_0_4b_7_ioh738", + "s_k_0_1s_3_irasis", + "s_k_0_a_1_ir4eod", + "s_n_0_9e_h_imjyqr", // Hunter Guild roof + walls (seems to remove slightly too much). + "s_n_0_40_a_it58n7", // Neon signs throughout the city. + "s_n_0_2m_1_isvawv", + "s_n_0_o_1_iwk2nr", + "a_n_0_2k_5_iyebd3", + "s_n_0_4_1_ikyjfd", + "s_n_0_g_1_iom8uk", + "s_n_0_j5_b_ivdcj1", + "s_n_0_28_1_iopx1k", + "s_m_0_3q_6_iqmvjr", + "s_m_0_26_2_inh1ma", + "s_m_0_4b_4_immz8l", + "s_m_0_22_2_ilwnn5", + "s_m_0_84_e_iv6noc", + "s_m_0_d_1_ili3v2", + "s_m_0_58_2_igd0am", + "s_m_0_25_3_iovf21", + "s_n_0_8_1_ik11uc", + "s_m_0_19_1_ijocvh", + "s_m_0_2h_5_is8o4b", + "s_m_0_1l_4_ilkky7", + "s_m_0_35_1_il8hoa", + "s_m_0_58_3_in4nwl", + "s_m_0_3d_1_iro50a", + "s_m_0_4_1_is53va", + "s_m_0_3l_6_igzvga", + "s_n_0_en_3_iiawrz", + ), ), // Forest 1 - Pair(Episode.I, 1) to setOf(12, 41), + Pair(Episode.I, 1) to Fix( + renderOnTopTextures = jsSetOf(12, 41), + ), + // Cave 1 + Pair(Episode.I, 3) to Fix( + hiddenObjects = jsSetOf( + "s_n_0_8_1_iqrqjj", + "s_i_0_b5_1_is7ajh", + "s_n_0_24_1_in5ce2", + "s_n_0_u_3_im4944", + "s_n_0_1b_2_im4945", + "s_n_0_2b_1_iktmat", + "s_n_0_3c_1_iksavp", + "s_n_0_31_1_ijhyzw", + "s_n_0_2i_3_ik3g7o", + "s_n_0_39_1_ix3ls0", + "s_n_0_37_1_ix3nxi", + "s_n_0_8x_1_iw2lqw", + "s_n_0_8w_1_ivx9ro", + "s_n_0_2c_1_itkfue", + "s_n_0_2u_1_iuilbk", + "s_n_0_30_1_ivmffx", + "s_n_0_2o_1_iu42tg", + "s_n_0_1u_1_ipk1qq", + "s_n_0_3i_1_iuz9mq", + "s_n_0_36_1_itm5fi", + "s_n_0_2o_1_ircjgr", + "s_n_0_3i_1_iurb4o", + "s_n_0_22_1_ii9035", + "s_n_0_2i_3_iiqupy", + ), + ), + // Cave 2 + Pair(Episode.I, 4) to Fix( + hiddenObjects = jsSetOf( + "s_n_0_4j_1_irf90i", + "s_n_0_5i_1_iqqrft", + "s_n_0_g_1_iipv9r", + "s_n_0_c_1_ihboen", + ), + ), + // Cave 3 + Pair(Episode.I, 5) to Fix( + hiddenObjects = jsSetOf( + "s_n_0_2o_5_inun1c", + "s_n_0_5y_2_ipyair", + ), + ), + // Mine 1 + Pair(Episode.I, 6) to Fix( + hiddenObjects = jsSetOf( + "s_n_0_2e_2_iqfpg8", + "s_n_0_d_1_iruof6", + "s_n_0_o_1_im9ta5", + "s_n_0_18_3_im1kwg", + ), + ), // Mine 2 - Pair(Episode.I, 7) to setOf(0, 1, 7, 8, 17, 23, 56, 57, 58, 59, 60, 83), + Pair(Episode.I, 7) to Fix( + renderOnTopTextures = jsSetOf(0, 1, 7, 8, 17, 23, 56, 57, 58, 59, 60, 83), + ), // Ruins 1 - Pair(Episode.I, 8) to setOf(1, 21, 22, 27, 28, 43, 51, 59, 70, 72, 75), + Pair(Episode.I, 8) to Fix( + renderOnTopTextures = jsSetOf(1, 21, 22, 27, 28, 43, 51, 59, 70, 72, 75), + hiddenObjects = jsSetOf( + "s_n_0_2p_4_iohs6r", + "s_n_0_2q_4_iohs6r", + "s_m_0_l_1_io448k", + ), + ), + // Ruins 2 + Pair(Episode.I, 9) to Fix( + hiddenObjects = jsSetOf( + "s_m_0_l_1_io448k", + ), + ), // Lab - Pair(Episode.II, 0) to setOf(36, 37, 38, 48, 60, 67, 79, 80), + Pair(Episode.II, 0) to Fix( + renderOnTopTextures = jsSetOf(36, 37, 38, 48, 60, 67, 79, 80), + ), + // VR Spaceship Alpha + Pair(Episode.II, 3) to Fix( + renderOnTopTextures = jsSetOf(7, 59), + hiddenObjects = jsSetOf( + "s_l_0_45_5_ing07n", + "s_n_0_45_5_ing07k", + "s_n_0_g2_b_im2en1", + "s_n_0_3j_1_irr4qe", + "s_n_0_bp_8_irbqmy", + "s_n_0_4h_1_irkudv", + "s_n_0_4g_1_irkudv", + "s_n_0_l_1_ijtl6r", + "s_n_0_l_1_ijtl6u", + "s_n_0_1s_1_imgj8o", + "s_n_0_r_1_ijua1b", + "s_n_0_g0_c_ilpett", + "s_n_0_16_1_igxq22", + "s_n_0_1c_1_imgj8o", + "s_n_0_1c_1_imgj8p", + "s_n_0_1u_1_imgj8o", + "s_n_0_1u_1_imgj8p", + "s_n_0_20_1_im13wb", + "s_n_0_12_1_ilsbgy", + "s_n_0_8_1_ihmjxh", + "s_n_0_1u_1_imv5rn", + "s_i_0_2d_4_ir3kzk", + "s_g_0_2d_4_ir3kzk", + "s_n_0_1t_1_imgj8o", + "s_n_0_l_1_ijoqlv", + "s_m_0_c_1_iayi9w", + "s_k_0_c_1_iayi9w", + "s_n_0_gl_8_imtj35", + "s_n_0_gc_8_imtj35", + "s_n_0_g_1_ildjm9", + ), + ), // Central Control Area - Pair(Episode.II, 5) to (0..59).toSet() + setOf(69, 77), - ) + Pair(Episode.II, 5) to Fix( + renderOnTopTextures = jsSetOf(*((0..59).toSet() + setOf(69, 77)).toTypedArray()), + ), + // Jungle Area East + Pair(Episode.II, 6) to Fix( + renderOnTopTextures = jsSetOf(0, 1, 2, 18, 21, 24), + ), + ).also { + // VR Spaceship Beta = VR Spaceship Alpha + it[Pair(Episode.II, 4)] = it[Pair(Episode.II, 3)]!! + } private val raycaster = Raycaster() private val tmpVec = Vector3() diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/stores/ViewerStore.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/stores/ViewerStore.kt index e26812bb..2a86bfb1 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/stores/ViewerStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/stores/ViewerStore.kt @@ -3,8 +3,8 @@ package world.phantasmal.web.viewer.stores import kotlinx.coroutines.launch import mu.KotlinLogging import world.phantasmal.core.enumValueOfOrNull +import world.phantasmal.lib.fileFormats.AreaGeometry import world.phantasmal.lib.fileFormats.CollisionGeometry -import world.phantasmal.lib.fileFormats.RenderGeometry import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NjMotion import world.phantasmal.lib.fileFormats.ninja.NjObject @@ -29,7 +29,7 @@ private val logger = KotlinLogging.logger {} sealed class NinjaGeometry { class Object(val obj: NinjaObject<*, *>) : NinjaGeometry() - class Render(val geometry: RenderGeometry) : NinjaGeometry() + class Render(val geometry: AreaGeometry) : NinjaGeometry() class Collision(val geometry: CollisionGeometry) : NinjaGeometry() }