From 60d0bc611638ec5dbd52d5a88f649fef2d372ca4 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 4 Apr 2021 22:58:39 +0200 Subject: [PATCH] Fixed XJ loading bug in the viewer. --- .../lib/fileFormats/AreaGeometry.kt | 9 ++-- .../phantasmal/lib/fileFormats/ninja/Ninja.kt | 41 +++++++++------ .../lib/fileFormats/ninja/NinjaObject.kt | 52 +++++++++++++++---- .../rendering/conversion/NinjaAnimation.kt | 2 +- .../conversion/NinjaGeometryConversion.kt | 22 ++++++-- .../questEditor/loading/AreaAssetLoader.kt | 7 ++- .../questEditor/loading/EntityAssetLoader.kt | 6 +-- .../controllers/ViewerToolbarController.kt | 2 +- .../loading/CharacterClassAssetLoader.kt | 16 +++--- .../web/viewer/rendering/MeshRenderer.kt | 41 +++++++++------ .../web/viewer/stores/ViewerStore.kt | 6 +-- 11 files changed, 134 insertions(+), 70 deletions(-) diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt index 8e300a31..8fb6ee2c 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt @@ -2,8 +2,7 @@ package world.phantasmal.lib.fileFormats import world.phantasmal.core.isBitSet import world.phantasmal.lib.cursor.Cursor -import world.phantasmal.lib.fileFormats.ninja.NinjaObject -import world.phantasmal.lib.fileFormats.ninja.XjModel +import world.phantasmal.lib.fileFormats.ninja.XjObject import world.phantasmal.lib.fileFormats.ninja.angleToRad import world.phantasmal.lib.fileFormats.ninja.parseXjObject @@ -15,7 +14,7 @@ class RenderSection( val id: Int, val position: Vec3, val rotation: Vec3, - val objects: List>, + val objects: List, ) fun parseAreaGeometry(cursor: Cursor): RenderObject { @@ -73,8 +72,8 @@ private fun parseGeometryTable( cursor: Cursor, tableOffset: Int, tableEntryCount: Int, -): List> { - val objects = mutableListOf>() +): List { + val objects = mutableListOf() for (i in 0 until tableEntryCount) { cursor.seekStart(tableOffset + 16 * i) 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 bd225f11..399aa610 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 @@ -11,29 +11,39 @@ import world.phantasmal.lib.fileFormats.vec3Float private const val NJCM: Int = 0x4D434A4E -fun parseNj(cursor: Cursor): PwResult>> = - parseNinja(cursor, ::parseNjModel, mutableMapOf()) +fun parseNj(cursor: Cursor): PwResult> = + parseNinja(cursor, ::parseNjModel, ::NjObject, mutableMapOf()) -fun parseXj(cursor: Cursor): PwResult>> = - parseNinja(cursor, { c, _ -> parseXjModel(c) }, Unit) +fun parseXj(cursor: Cursor): PwResult> = + parseNinja(cursor, { c, _ -> parseXjModel(c) }, ::XjObject, Unit) -fun parseXjObject(cursor: Cursor): List> = - parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, Unit) +fun parseXjObject(cursor: Cursor): List = + parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, ::XjObject, Unit) -private fun parseNinja( +private typealias CreateObject = ( + evaluationFlags: NinjaEvaluationFlags, + model: Model?, + position: Vec3, + rotation: Vec3, + scale: Vec3, + children: MutableList, +) -> Obj + +private fun , Context> parseNinja( cursor: Cursor, parseModel: (cursor: Cursor, context: Context) -> Model, + createObject: CreateObject, context: Context, -): PwResult>> = +): PwResult> = when (val parseIffResult = parseIff(cursor)) { is Failure -> parseIffResult is Success -> { // POF0 and other chunks types are ignored. val njcmChunks = parseIffResult.value.filter { chunk -> chunk.type == NJCM } - val objects: MutableList> = mutableListOf() + val objects: MutableList = mutableListOf() for (chunk in njcmChunks) { - objects.addAll(parseSiblingObjects(chunk.data, parseModel, context)) + objects.addAll(parseSiblingObjects(chunk.data, parseModel, createObject, context)) } Success(objects, parseIffResult.problems) @@ -41,11 +51,12 @@ private fun parseNinja( } // TODO: cache model and object offsets so we don't reparse the same data. -private fun parseSiblingObjects( +private fun , Context> parseSiblingObjects( cursor: Cursor, parseModel: (cursor: Cursor, context: Context) -> Model, + createObject: CreateObject, context: Context, -): MutableList> { +): MutableList { val evalFlags = cursor.int() val noTranslate = evalFlags.isBitSet(0) val noRotate = evalFlags.isBitSet(1) @@ -80,17 +91,17 @@ private fun parseSiblingObjects( mutableListOf() } else { cursor.seekStart(childOffset) - parseSiblingObjects(cursor, parseModel, context) + parseSiblingObjects(cursor, parseModel, createObject, context) } val siblings = if (siblingOffset == 0) { mutableListOf() } else { cursor.seekStart(siblingOffset) - parseSiblingObjects(cursor, parseModel, context) + parseSiblingObjects(cursor, parseModel, createObject, context) } - val obj = NinjaObject( + val obj = createObject( NinjaEvaluationFlags( noTranslate, noRotate, 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 8d35a5c9..b323edba 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 @@ -3,7 +3,7 @@ package world.phantasmal.lib.fileFormats.ninja import world.phantasmal.lib.fileFormats.Vec2 import world.phantasmal.lib.fileFormats.Vec3 -class NinjaObject( +sealed class NinjaObject>( val evaluationFlags: NinjaEvaluationFlags, val model: Model?, val position: Vec3, @@ -12,29 +12,31 @@ class NinjaObject( */ val rotation: Vec3, val scale: Vec3, - children: MutableList>, + children: MutableList, ) { private val _children = children - val children: List> = _children + val children: List = _children - fun addChild(child: NinjaObject) { + fun addChild(child: Self) { _children.add(child) } fun boneCount(): Int { val indexRef = intArrayOf(0) - findBone(this, Int.MAX_VALUE, indexRef) + @Suppress("UNCHECKED_CAST") + findBone(this as Self, Int.MAX_VALUE, indexRef) return indexRef[0] } - fun getBone(index: Int): NinjaObject? = - findBone(this, index, intArrayOf(0)) + fun getBone(index: Int): Self? = + @Suppress("UNCHECKED_CAST") + findBone(this as Self, index, intArrayOf(0)) private fun findBone( - obj: NinjaObject, + obj: Self, boneIndex: Int, indexRef: IntArray, - ): NinjaObject? { + ): Self? { if (!obj.evaluationFlags.skip) { val index = indexRef[0]++ @@ -54,6 +56,38 @@ class NinjaObject( } } +class NjObject( + evaluationFlags: NinjaEvaluationFlags, + model: NjModel?, + position: Vec3, + rotation: Vec3, + scale: Vec3, + children: MutableList, +) : NinjaObject( + evaluationFlags, + model, + position, + rotation, + scale, + children, +) + +class XjObject( + evaluationFlags: NinjaEvaluationFlags, + model: XjModel?, + position: Vec3, + rotation: Vec3, + scale: Vec3, + children: MutableList, +) : NinjaObject( + evaluationFlags, + model, + position, + rotation, + scale, + children, +) + class NinjaEvaluationFlags( var noTranslate: Boolean, var noRotate: Boolean, diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaAnimation.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaAnimation.kt index 5e55cb35..73b9a22d 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaAnimation.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaAnimation.kt @@ -11,7 +11,7 @@ import world.phantasmal.web.externals.three.* const val PSO_FRAME_RATE: Int = 30 const val PSO_FRAME_RATE_DOUBLE: Double = PSO_FRAME_RATE.toDouble() -fun createAnimationClip(njObject: NinjaObject<*>, njMotion: NjMotion): AnimationClip { +fun createAnimationClip(njObject: NinjaObject<*, *>, njMotion: NjMotion): AnimationClip { val interpolation = if (njMotion.interpolation == NjInterpolation.Spline) InterpolateSmooth else InterpolateLinear 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 c38ab765..cb32a4b8 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 @@ -19,8 +19,20 @@ private val tmpNormal = Vector3() private val tmpVec = Vector3() private val tmpNormalMatrix = Matrix3() +fun ninjaObjectToMesh( + ninjaObject: NinjaObject<*, *>, + textures: List, + defaultMaterial: Material? = null, + boundingVolumes: Boolean = false, +): Mesh { + val builder = MeshBuilder(textures) + defaultMaterial?.let { builder.defaultMaterial(defaultMaterial) } + ninjaObjectToMeshBuilder(ninjaObject, builder) + return builder.buildMesh(boundingVolumes) +} + fun ninjaObjectToInstancedMesh( - ninjaObject: NinjaObject<*>, + ninjaObject: NinjaObject<*, *>, textures: List, maxInstances: Int, defaultMaterial: Material? = null, @@ -33,7 +45,7 @@ fun ninjaObjectToInstancedMesh( } fun ninjaObjectToSkinnedMesh( - ninjaObject: NinjaObject<*>, + ninjaObject: NjObject, textures: List, defaultMaterial: Material? = null, boundingVolumes: Boolean = false, @@ -45,7 +57,7 @@ fun ninjaObjectToSkinnedMesh( } fun ninjaObjectToMeshBuilder( - ninjaObject: NinjaObject<*>, + ninjaObject: NinjaObject<*, *>, builder: MeshBuilder, ) { NinjaToMeshConverter(builder).convert(ninjaObject) @@ -56,11 +68,11 @@ private class NinjaToMeshConverter(private val builder: MeshBuilder) { private val vertexHolder = VertexHolder() private var boneIndex = 0 - fun convert(ninjaObject: NinjaObject<*>) { + fun convert(ninjaObject: NinjaObject<*, *>) { convertObject(ninjaObject, null, Matrix4()) } - private fun convertObject(obj: NinjaObject<*>, parentBone: Bone?, parentMatrix: Matrix4) { + private fun convertObject(obj: NinjaObject<*, *>, parentBone: Bone?, parentMatrix: Matrix4) { val ef = obj.evaluationFlags val euler = Euler( 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 ae2920bb..54e5ab75 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 @@ -12,8 +12,7 @@ import world.phantasmal.lib.Episode import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.fileFormats.CollisionObject import world.phantasmal.lib.fileFormats.RenderObject -import world.phantasmal.lib.fileFormats.ninja.NinjaObject -import world.phantasmal.lib.fileFormats.ninja.XjModel +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 @@ -222,7 +221,7 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } private fun shouldRenderOnTop( - obj: NinjaObject, + obj: XjObject, episode: Episode, areaVariant: AreaVariantModel, ): Boolean { @@ -251,7 +250,7 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine return false } - fun recurse(obj: NinjaObject): Boolean { + fun recurse(obj: XjObject): Boolean { obj.model?.meshes?.let { meshes -> for (mesh in meshes) { mesh.material.textureId?.let { diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt index 969c0068..5a9db7a4 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt @@ -94,11 +94,11 @@ class EntityAssetLoader(private val assetLoader: AssetLoader) : DisposableContai } } - private fun parseGeometry( + private fun > parseGeometry( type: EntityType, parts: List>, - parse: (Cursor) -> PwResult>>, - ): NinjaObject? { + parse: (Cursor) -> PwResult>, + ): Obj? { val ninjaObjects = parts.flatMap { (path, data) -> val njObjects = parse(data.cursor(Endianness.Little)) diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/controllers/ViewerToolbarController.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/controllers/ViewerToolbarController.kt index ac78734e..c1a76e30 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/controllers/ViewerToolbarController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/controllers/ViewerToolbarController.kt @@ -62,7 +62,7 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { var success = false try { - var ninjaObject: NinjaObject<*>? = null + var ninjaObject: NinjaObject<*, *>? = null var textures: List? = null var ninjaMotion: NjMotion? = null diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/loading/CharacterClassAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/loading/CharacterClassAssetLoader.kt index 2d37a942..5372a5af 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/loading/CharacterClassAssetLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/loading/CharacterClassAssetLoader.kt @@ -13,13 +13,13 @@ import world.phantasmal.web.viewer.models.CharacterClass.* import world.phantasmal.webui.DisposableContainer class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : DisposableContainer() { - private val ninjaObjectCache: LoadingCache> = + private val ninjaObjectCache: LoadingCache = addDisposable(LoadingCache(::loadBodyParts) { /* Nothing to dispose. */ }) private val xvrTextureCache: LoadingCache> = addDisposable(LoadingCache(::loadTextures) { /* Nothing to dispose. */ }) - suspend fun loadNinjaObject(char: CharacterClass): NinjaObject = + suspend fun loadNinjaObject(char: CharacterClass): NjObject = ninjaObjectCache.get(char) suspend fun loadXvrTextures( @@ -42,7 +42,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab /** * Loads the separate body parts and joins them together at the right bones. */ - private suspend fun loadBodyParts(char: CharacterClass): NinjaObject { + private suspend fun loadBodyParts(char: CharacterClass): NjObject { val texIds = textureIds(char, SectionId.Viridia, 0) val body = loadBodyPart(char, "Body") @@ -77,7 +77,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab char: CharacterClass, bodyPart: String, no: Int? = null, - ): NinjaObject { + ): NjObject { val buffer = assetLoader.loadArrayBuffer("/player/${char.slug}${bodyPart}${no ?: ""}.nj") return parseNj(buffer.cursor(Endianness.Little)).unwrap().first() } @@ -85,7 +85,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab /** * Shift texture IDs so that the IDs of different body parts don't overlap. */ - private fun shiftTextureIds(njObject: NinjaObject, shift: Int) { + private fun shiftTextureIds(njObject: NjObject, shift: Int) { njObject.model?.let { model -> for (mesh in model.meshes) { mesh.textureId = mesh.textureId?.plus(shift) @@ -97,9 +97,9 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab } } - private fun addToBone( - obj: NinjaObject, - child: NinjaObject, + private fun addToBone( + obj: NjObject, + child: NjObject, parentBoneId: Int, ) { obj.getBone(parentBoneId)?.let { bone -> diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt index 6aa90a1e..ebfb8dba 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt @@ -5,10 +5,12 @@ import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.core.math.degToRad import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NjMotion +import world.phantasmal.lib.fileFormats.ninja.NjObject import world.phantasmal.web.core.rendering.* import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.core.rendering.conversion.PSO_FRAME_RATE_DOUBLE import world.phantasmal.web.core.rendering.conversion.createAnimationClip +import world.phantasmal.web.core.rendering.conversion.ninjaObjectToMesh import world.phantasmal.web.core.rendering.conversion.ninjaObjectToSkinnedMesh import world.phantasmal.web.core.times import world.phantasmal.web.externals.three.* @@ -91,7 +93,7 @@ class MeshRenderer( skeletonHelper = null } - val njObject = viewerStore.currentNinjaObject.value + val ninjaObject = viewerStore.currentNinjaObject.value val textures = viewerStore.currentTextures.value // Stop and clean up previous animation and store animation time. @@ -104,8 +106,13 @@ class MeshRenderer( } // Create a new mesh if necessary. - if (njObject != null) { - val mesh = ninjaObjectToSkinnedMesh(njObject, textures, boundingVolumes = true) + if (ninjaObject != null) { + val mesh = + if (ninjaObject is NjObject) { + ninjaObjectToSkinnedMesh(ninjaObject, textures, boundingVolumes = true) + } else { + ninjaObjectToMesh(ninjaObject, textures, boundingVolumes = true) + } // Determine whether camera needs to be reset. Resets should always happen when the // Ninja object changes except when we're switching between character class models. @@ -125,20 +132,22 @@ class MeshRenderer( context.scene.add(mesh) this.mesh = mesh - // Add skeleton. - val skeletonHelper = SkeletonHelper(mesh) - skeletonHelper.visible = viewerStore.showSkeleton.value - skeletonHelper.asDynamic().material.lineWidth = 3 + if (mesh is SkinnedMesh) { + // Add skeleton. + val skeletonHelper = SkeletonHelper(mesh) + skeletonHelper.visible = viewerStore.showSkeleton.value + skeletonHelper.asDynamic().material.lineWidth = 3 - context.scene.add(skeletonHelper) - this.skeletonHelper = skeletonHelper + context.scene.add(skeletonHelper) + this.skeletonHelper = skeletonHelper - // Create a new animation mixer and clip. - viewerStore.currentNinjaMotion.value?.let { njMotion -> - animation = Animation(njObject, njMotion, mesh).also { - it.mixer.timeScale = viewerStore.frameRate.value / PSO_FRAME_RATE_DOUBLE - it.action.time = animationTime ?: .0 - it.action.play() + // Create a new animation mixer and clip. + viewerStore.currentNinjaMotion.value?.let { njMotion -> + animation = Animation(ninjaObject, njMotion, mesh).also { + it.mixer.timeScale = viewerStore.frameRate.value / PSO_FRAME_RATE_DOUBLE + it.action.time = animationTime ?: .0 + it.action.play() + } } } } @@ -192,7 +201,7 @@ class MeshRenderer( } private class Animation( - njObject: NinjaObject<*>, + njObject: NinjaObject<*, *>, njMotion: NjMotion, root: Object3D, ) : TrackedDisposable() { 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 6ffb03e3..a3bfdff2 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 @@ -29,7 +29,7 @@ class ViewerStore( uiStore: UiStore, ) : Store() { // Ninja concepts. - private val _currentNinjaObject = mutableVal?>(null) + private val _currentNinjaObject = mutableVal?>(null) private val _currentTextures = mutableListVal() private val _currentNinjaMotion = mutableVal(null) @@ -47,7 +47,7 @@ class ViewerStore( private val _frame = mutableVal(0) // Ninja concepts. - val currentNinjaObject: Val?> = _currentNinjaObject + val currentNinjaObject: Val?> = _currentNinjaObject val currentTextures: ListVal = _currentTextures val currentNinjaMotion: Val = _currentNinjaMotion @@ -143,7 +143,7 @@ class ViewerStore( } } - fun setCurrentNinjaObject(ninjaObject: NinjaObject<*>?) { + fun setCurrentNinjaObject(ninjaObject: NinjaObject<*, *>?) { if (_currentCharacterClass.value != null) { _currentCharacterClass.value = null _currentTextures.clear()