Fixed XJ loading bug in the viewer.

This commit is contained in:
Daan Vanden Bosch 2021-04-04 22:58:39 +02:00
parent 0ff4752949
commit 60d0bc6116
11 changed files with 134 additions and 70 deletions

View File

@ -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<NinjaObject<XjModel>>,
val objects: List<XjObject>,
)
fun parseAreaGeometry(cursor: Cursor): RenderObject {
@ -73,8 +72,8 @@ private fun parseGeometryTable(
cursor: Cursor,
tableOffset: Int,
tableEntryCount: Int,
): List<NinjaObject<XjModel>> {
val objects = mutableListOf<NinjaObject<XjModel>>()
): List<XjObject> {
val objects = mutableListOf<XjObject>()
for (i in 0 until tableEntryCount) {
cursor.seekStart(tableOffset + 16 * i)

View File

@ -11,29 +11,39 @@ import world.phantasmal.lib.fileFormats.vec3Float
private const val NJCM: Int = 0x4D434A4E
fun parseNj(cursor: Cursor): PwResult<List<NinjaObject<NjModel>>> =
parseNinja(cursor, ::parseNjModel, mutableMapOf())
fun parseNj(cursor: Cursor): PwResult<List<NjObject>> =
parseNinja(cursor, ::parseNjModel, ::NjObject, mutableMapOf())
fun parseXj(cursor: Cursor): PwResult<List<NinjaObject<XjModel>>> =
parseNinja(cursor, { c, _ -> parseXjModel(c) }, Unit)
fun parseXj(cursor: Cursor): PwResult<List<XjObject>> =
parseNinja(cursor, { c, _ -> parseXjModel(c) }, ::XjObject, Unit)
fun parseXjObject(cursor: Cursor): List<NinjaObject<XjModel>> =
parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, Unit)
fun parseXjObject(cursor: Cursor): List<XjObject> =
parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, ::XjObject, Unit)
private fun <Model : NinjaModel, Context> parseNinja(
private typealias CreateObject<Model, Obj> = (
evaluationFlags: NinjaEvaluationFlags,
model: Model?,
position: Vec3,
rotation: Vec3,
scale: Vec3,
children: MutableList<Obj>,
) -> Obj
private fun <Model : NinjaModel, Obj : NinjaObject<Model, Obj>, Context> parseNinja(
cursor: Cursor,
parseModel: (cursor: Cursor, context: Context) -> Model,
createObject: CreateObject<Model, Obj>,
context: Context,
): PwResult<List<NinjaObject<Model>>> =
): PwResult<List<Obj>> =
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<NinjaObject<Model>> = mutableListOf()
val objects: MutableList<Obj> = 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 <Model : NinjaModel, Context> parseNinja(
}
// TODO: cache model and object offsets so we don't reparse the same data.
private fun <Model : NinjaModel, Context> parseSiblingObjects(
private fun <Model : NinjaModel, Obj : NinjaObject<Model, Obj>, Context> parseSiblingObjects(
cursor: Cursor,
parseModel: (cursor: Cursor, context: Context) -> Model,
createObject: CreateObject<Model, Obj>,
context: Context,
): MutableList<NinjaObject<Model>> {
): MutableList<Obj> {
val evalFlags = cursor.int()
val noTranslate = evalFlags.isBitSet(0)
val noRotate = evalFlags.isBitSet(1)
@ -80,17 +91,17 @@ private fun <Model : NinjaModel, Context> 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,

View File

@ -3,7 +3,7 @@ package world.phantasmal.lib.fileFormats.ninja
import world.phantasmal.lib.fileFormats.Vec2
import world.phantasmal.lib.fileFormats.Vec3
class NinjaObject<Model : NinjaModel>(
sealed class NinjaObject<Model : NinjaModel, Self : NinjaObject<Model, Self>>(
val evaluationFlags: NinjaEvaluationFlags,
val model: Model?,
val position: Vec3,
@ -12,29 +12,31 @@ class NinjaObject<Model : NinjaModel>(
*/
val rotation: Vec3,
val scale: Vec3,
children: MutableList<NinjaObject<Model>>,
children: MutableList<Self>,
) {
private val _children = children
val children: List<NinjaObject<Model>> = _children
val children: List<Self> = _children
fun addChild(child: NinjaObject<Model>) {
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<Model>? =
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<Model>,
obj: Self,
boneIndex: Int,
indexRef: IntArray,
): NinjaObject<Model>? {
): Self? {
if (!obj.evaluationFlags.skip) {
val index = indexRef[0]++
@ -54,6 +56,38 @@ class NinjaObject<Model : NinjaModel>(
}
}
class NjObject(
evaluationFlags: NinjaEvaluationFlags,
model: NjModel?,
position: Vec3,
rotation: Vec3,
scale: Vec3,
children: MutableList<NjObject>,
) : NinjaObject<NjModel, NjObject>(
evaluationFlags,
model,
position,
rotation,
scale,
children,
)
class XjObject(
evaluationFlags: NinjaEvaluationFlags,
model: XjModel?,
position: Vec3,
rotation: Vec3,
scale: Vec3,
children: MutableList<XjObject>,
) : NinjaObject<XjModel, XjObject>(
evaluationFlags,
model,
position,
rotation,
scale,
children,
)
class NinjaEvaluationFlags(
var noTranslate: Boolean,
var noRotate: Boolean,

View File

@ -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

View File

@ -19,8 +19,20 @@ private val tmpNormal = Vector3()
private val tmpVec = Vector3()
private val tmpNormalMatrix = Matrix3()
fun ninjaObjectToMesh(
ninjaObject: NinjaObject<*, *>,
textures: List<XvrTexture?>,
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<XvrTexture>,
maxInstances: Int,
defaultMaterial: Material? = null,
@ -33,7 +45,7 @@ fun ninjaObjectToInstancedMesh(
}
fun ninjaObjectToSkinnedMesh(
ninjaObject: NinjaObject<*>,
ninjaObject: NjObject,
textures: List<XvrTexture?>,
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(

View File

@ -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<XjModel>,
obj: XjObject,
episode: Episode,
areaVariant: AreaVariantModel,
): Boolean {
@ -251,7 +250,7 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
return false
}
fun recurse(obj: NinjaObject<XjModel>): Boolean {
fun recurse(obj: XjObject): Boolean {
obj.model?.meshes?.let { meshes ->
for (mesh in meshes) {
mesh.material.textureId?.let {

View File

@ -94,11 +94,11 @@ class EntityAssetLoader(private val assetLoader: AssetLoader) : DisposableContai
}
}
private fun <Model : NinjaModel> parseGeometry(
private fun <Obj : NinjaObject<*, Obj>> parseGeometry(
type: EntityType,
parts: List<Pair<String, ArrayBuffer>>,
parse: (Cursor) -> PwResult<List<NinjaObject<Model>>>,
): NinjaObject<Model>? {
parse: (Cursor) -> PwResult<List<Obj>>,
): Obj? {
val ninjaObjects = parts.flatMap { (path, data) ->
val njObjects = parse(data.cursor(Endianness.Little))

View File

@ -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<XvrTexture>? = null
var ninjaMotion: NjMotion? = null

View File

@ -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<CharacterClass, NinjaObject<NjModel>> =
private val ninjaObjectCache: LoadingCache<CharacterClass, NjObject> =
addDisposable(LoadingCache(::loadBodyParts) { /* Nothing to dispose. */ })
private val xvrTextureCache: LoadingCache<CharacterClass, List<XvrTexture?>> =
addDisposable(LoadingCache(::loadTextures) { /* Nothing to dispose. */ })
suspend fun loadNinjaObject(char: CharacterClass): NinjaObject<NjModel> =
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<NjModel> {
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<NjModel> {
): 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<NjModel>, 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 <M : NinjaModel> addToBone(
obj: NinjaObject<M>,
child: NinjaObject<M>,
private fun addToBone(
obj: NjObject,
child: NjObject,
parentBoneId: Int,
) {
obj.getBone(parentBoneId)?.let { bone ->

View File

@ -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() {

View File

@ -29,7 +29,7 @@ class ViewerStore(
uiStore: UiStore,
) : Store() {
// Ninja concepts.
private val _currentNinjaObject = mutableVal<NinjaObject<*>?>(null)
private val _currentNinjaObject = mutableVal<NinjaObject<*, *>?>(null)
private val _currentTextures = mutableListVal<XvrTexture?>()
private val _currentNinjaMotion = mutableVal<NjMotion?>(null)
@ -47,7 +47,7 @@ class ViewerStore(
private val _frame = mutableVal(0)
// Ninja concepts.
val currentNinjaObject: Val<NinjaObject<*>?> = _currentNinjaObject
val currentNinjaObject: Val<NinjaObject<*, *>?> = _currentNinjaObject
val currentTextures: ListVal<XvrTexture?> = _currentTextures
val currentNinjaMotion: Val<NjMotion?> = _currentNinjaMotion
@ -143,7 +143,7 @@ class ViewerStore(
}
}
fun setCurrentNinjaObject(ninjaObject: NinjaObject<*>?) {
fun setCurrentNinjaObject(ninjaObject: NinjaObject<*, *>?) {
if (_currentCharacterClass.value != null) {
_currentCharacterClass.value = null
_currentTextures.clear()