diff --git a/FEATURES.md b/FEATURES.md index 4e6bc7eb..02068e90 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -157,16 +157,19 @@ Features that are in ***bold italics*** are planned but not yet implemented. ## Bugs - When a modal dialog is open, global keybindings should be disabled +- Wen right-click dragging from the 3D-view and releasing the mouse button outside the 3D-view, the + default context menu pops up +- Improve the default camera target for Crater Interior - Entities with rendering issues: - - Caves 4 Button door - - Pofuilly Slime - - Pouilly Slime - - Easter Egg - - Christmas Tree - - Halloween Pumpkin - - 21st Century - - Light rays - used in forest and CCA - - Big CCA Door Switch + - Caves 4 Button door + - Pofuilly Slime + - Pouilly Slime + - Easter Egg + - Christmas Tree + - Halloween Pumpkin + - 21st Century + - Light rays - used in forest and CCA + - Big CCA Door Switch - Laser Detect - used in CCA - Wide Glass Wall (breakable) - used in Seabed - item box cca diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaCollisionGeometry.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaCollisionGeometry.kt index 99099a20..f3225dfc 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaCollisionGeometry.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaCollisionGeometry.kt @@ -2,7 +2,7 @@ package world.phantasmal.lib.fileFormats import world.phantasmal.lib.cursor.Cursor -class CollisionObject( +class CollisionGeometry( val meshes: List, ) @@ -19,7 +19,7 @@ class CollisionTriangle( val normal: Vec3, ) -fun parseAreaCollisionGeometry(cursor: Cursor): CollisionObject { +fun parseAreaCollisionGeometry(cursor: Cursor): CollisionGeometry { val dataOffset = parseRel(cursor, parseIndex = false).dataOffset cursor.seekStart(dataOffset) val mainOffsetTableOffset = cursor.int() @@ -74,5 +74,5 @@ fun parseAreaCollisionGeometry(cursor: Cursor): CollisionObject { cursor.seekStart(startPos + 24) } - return CollisionObject(meshes) + return CollisionGeometry(meshes) } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt similarity index 95% rename from lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt rename to lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt index 8fb6ee2c..910bd52b 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaGeometry.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/AreaRenderGeometry.kt @@ -6,7 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.XjObject import world.phantasmal.lib.fileFormats.ninja.angleToRad import world.phantasmal.lib.fileFormats.ninja.parseXjObject -class RenderObject( +class RenderGeometry( val sections: List, ) @@ -17,7 +17,7 @@ class RenderSection( val objects: List, ) -fun parseAreaGeometry(cursor: Cursor): RenderObject { +fun parseAreaRenderGeometry(cursor: Cursor): RenderGeometry { val sections = mutableListOf() cursor.seekEnd(16) @@ -64,7 +64,7 @@ fun parseAreaGeometry(cursor: Cursor): RenderObject { )) } - return RenderObject(sections) + return RenderGeometry(sections) } // TODO: don't reparse the same objects multiple times. Create DAG instead of tree. diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 9a90585f..86aa5b72 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -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.126.0")) + implementation(npm("three", "^0.127.0")) implementation(npm("javascript-lp-solver", "0.4.17")) implementation(devNpm("file-loader", "^6.0.0")) diff --git a/web/src/main/kotlin/world/phantasmal/web/core/ThreeExtensions.kt b/web/src/main/kotlin/world/phantasmal/web/core/ThreeExtensions.kt index 7a7c40c4..f0b42e5b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/ThreeExtensions.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/ThreeExtensions.kt @@ -1,8 +1,10 @@ package world.phantasmal.web.core -import world.phantasmal.web.externals.three.Euler -import world.phantasmal.web.externals.three.Quaternion -import world.phantasmal.web.externals.three.Vector3 +import world.phantasmal.web.externals.three.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +private val tmpSphere = Sphere() operator fun Vector3.plus(other: Vector3): Vector3 = clone().add(other) @@ -58,3 +60,36 @@ fun euler(x: Double, y: Double, z: Double): Euler = */ fun Euler.toQuaternion(): Quaternion = Quaternion().setFromEuler(this) + +@OptIn(ExperimentalContracts::class) +inline fun Object3D.isMesh(): Boolean { + contract { + returns(true) implies (this@isMesh is Mesh) + } + + return unsafeCast().isMesh +} + +@OptIn(ExperimentalContracts::class) +inline fun Object3D.isSkinnedMesh(): Boolean { + contract { + returns(true) implies (this@isSkinnedMesh is SkinnedMesh) + } + + return unsafeCast().isSkinnedMesh +} + +fun boundingSphere(object3d: Object3D, bSphere: Sphere = Sphere()): Sphere { + if (object3d.isMesh()) { + // Don't use reference to union method to improve performance of emitted JS. + object3d.geometry.boundingSphere?.let { + tmpSphere.copy(it) + tmpSphere.applyMatrix4(object3d.matrixWorld) + bSphere.union(tmpSphere) + } + } + + object3d.children.forEach { boundingSphere(it, bSphere) } + + return bSphere +} 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 cb32a4b8..36b43635 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 @@ -1,10 +1,21 @@ package world.phantasmal.web.core.rendering.conversion import mu.KotlinLogging +import org.khronos.webgl.Float32Array +import org.khronos.webgl.Uint16Array +import world.phantasmal.core.JsArray +import world.phantasmal.core.asArray +import world.phantasmal.core.isBitSet +import world.phantasmal.core.jsArrayOf +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.ninja.* import world.phantasmal.web.core.dot import world.phantasmal.web.core.toQuaternion import world.phantasmal.web.externals.three.* +import world.phantasmal.webui.obj private val logger = KotlinLogging.logger {} @@ -14,6 +25,55 @@ private val NO_TRANSLATION = Vector3(0.0, 0.0, 0.0) private val NO_ROTATION = Quaternion() private val NO_SCALE = Vector3(1.0, 1.0, 1.0) +private val COLLISION_MATERIALS: Array = arrayOf( + // Wall + MeshBasicMaterial(obj { + color = Color(0x80c0d0) + transparent = true + opacity = .25 + }), + // Ground + MeshLambertMaterial(obj { + color = Color(0x405050) + side = DoubleSide + }), + // Vegetation + MeshLambertMaterial(obj { + color = Color(0x306040) + side = DoubleSide + }), + // Section transition zone + MeshLambertMaterial(obj { + color = Color(0x402050) + side = DoubleSide + }), +) + +private val COLLISION_WIREFRAME_MATERIALS: Array = arrayOf( + // Wall + MeshBasicMaterial(obj { + color = Color(0x90d0e0) + wireframe = true + transparent = true + opacity = .3 + }), + // Ground + MeshBasicMaterial(obj { + color = Color(0x506060) + wireframe = true + }), + // Vegetation + MeshBasicMaterial(obj { + color = Color(0x405050) + wireframe = true + }), + // Section transition zone + MeshBasicMaterial(obj { + color = Color(0x503060) + wireframe = true + }), +) + // Objects used for temporary calculations to avoid GC. private val tmpNormal = Vector3() private val tmpVec = Vector3() @@ -63,6 +123,117 @@ fun ninjaObjectToMeshBuilder( NinjaToMeshConverter(builder).convert(ninjaObject) } +fun renderGeometryToGroup( + renderGeometry: RenderGeometry, + textures: List, + processMesh: (RenderSection, XjObject, Mesh) -> Unit = { _, _, _ -> }, +): Group { + val group = Group() + val textureCache = mutableMapOf() + + for ((i, section) in renderGeometry.sections.withIndex()) { + for (xjObj in section.objects) { + val builder = MeshBuilder(textures, textureCache) + ninjaObjectToMeshBuilder(xjObj, builder) + + builder.defaultMaterial(MeshBasicMaterial(obj { + color = Color().setHSL((i % 7) / 7.0, 1.0, .5) + transparent = true + opacity = .25 + side = DoubleSide + })) + + val mesh = builder.buildMesh(boundingVolumes = true) + + mesh.position.setFromVec3(section.position) + mesh.rotation.setFromVec3(section.rotation) + mesh.updateMatrixWorld() + + processMesh(section, xjObj, mesh) + + group.add(mesh) + } + } + + return group +} + +fun collisionGeometryToGroup( + collisionGeometry: CollisionGeometry, + trianglePredicate: (CollisionTriangle) -> Boolean = { true }, +): Group { + val group = Group() + + for (collisionMesh in collisionGeometry.meshes) { + val positions = jsArrayOf() + val normals = jsArrayOf() + val materialGroups = mutableMapOf>() + var index: Short = 0 + + for (triangle in collisionMesh.triangles) { + // This a vague approximation of the real meaning of these flags. + val isGround = triangle.flags.isBitSet(0) + val isVegetation = triangle.flags.isBitSet(4) + val isSectionTransition = triangle.flags.isBitSet(6) + val materialIndex = when { + isSectionTransition -> 3 + isVegetation -> 2 + isGround -> 1 + else -> 0 + } + + if (trianglePredicate(triangle)) { + 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 (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() + + group.add( + Mesh(geom, COLLISION_MATERIALS).apply { + renderOrder = 1 + } + ) + + group.add( + Mesh(geom, COLLISION_WIREFRAME_MATERIALS).apply { + renderOrder = 2 + } + ) + } + } + + return group +} + // TODO: take into account different kinds of meshes/vertices (with or without normals, uv, etc.). private class NinjaToMeshConverter(private val builder: MeshBuilder) { private val vertexHolder = VertexHolder() diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt b/web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt index 3766921e..4b967ef3 100644 --- a/web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt +++ b/web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt @@ -187,12 +187,25 @@ external class Box3(min: Vector3 = definedExternally, max: Vector3 = definedExte var min: Vector3 var max: Vector3 + fun applyMatrix4(matrix: Matrix4): Box3 + + fun copy(box: Box3): Box3 + fun getCenter(target: Vector3): Vector3 + + fun intersectsBox(box: Box3): Boolean + + fun union(box: Box3): Box3 } external class Sphere(center: Vector3 = definedExternally, radius: Double = definedExternally) { var center: Vector3 var radius: Double + + fun applyMatrix4(matrix: Matrix4): Sphere + fun clone(): Sphere + fun copy(sphere: Sphere): Sphere + fun union(sphere: Sphere): Sphere } open external class EventDispatcher @@ -274,6 +287,7 @@ open external class Object3D { * Local transform. */ var matrix: Matrix4 + var matrixWorld: Matrix4 var visible: Boolean @@ -315,6 +329,7 @@ open external class Mesh( material: Array, ) + val isMesh: Boolean var geometry: BufferGeometry var material: Any /* Material | Material[] */ @@ -332,6 +347,7 @@ external class SkinnedMesh( useVertexTexture: Boolean = definedExternally, ) + val isSkinnedMesh: Boolean val skeleton: Skeleton fun bind(skeleton: Skeleton, bindMatrix: Matrix4 = definedExternally) @@ -379,6 +395,8 @@ open external class BoxHelper( fun setFromObject(`object`: Object3D): BoxHelper } +external class Box3Helper(box: Box3, color: Color = definedExternally) : LineSegments + external class Scene : Object3D { var background: dynamic /* null | Color | Texture | WebGLCubeRenderTarget */ } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/actions/CreateEntityAction.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/actions/CreateEntityAction.kt index b415cf1a..71bff2eb 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/actions/CreateEntityAction.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/actions/CreateEntityAction.kt @@ -9,7 +9,7 @@ class CreateEntityAction( private val quest: QuestModel, private val entity: QuestEntityModel<*, *>, ) : Action { - override val description: String = "Create ${entity.type.name}" + override val description: String = "Add ${entity.type.name}" override fun execute() { quest.addEntity(entity) 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 54e5ab75..b65064a4 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,22 +1,19 @@ 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.core.isBitSet import world.phantasmal.lib.Endianness 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.CollisionGeometry +import world.phantasmal.lib.fileFormats.RenderGeometry 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.parseAreaGeometry +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.* import world.phantasmal.web.core.rendering.disposeObject3DResources @@ -24,7 +21,8 @@ import world.phantasmal.web.externals.three.* import world.phantasmal.web.questEditor.models.AreaVariantModel import world.phantasmal.web.questEditor.models.SectionModel import world.phantasmal.webui.DisposableContainer -import world.phantasmal.webui.obj +import kotlin.math.PI +import kotlin.math.cos interface AreaUserData { var section: SectionModel? @@ -34,53 +32,53 @@ interface AreaUserData { * Loads and caches area assets. */ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContainer() { - /** - * This cache's values consist of an Object3D containing the area render meshes and a list of - * that area's sections. - */ - private val renderObjectCache = addDisposable( - LoadingCache>>( + private val cache = addDisposable( + LoadingCache( { (episode, areaVariant) -> - val obj = parseAreaGeometry( + val renderObj = parseAreaRenderGeometry( getAreaAsset(episode, areaVariant, AssetType.Render).cursor(Endianness.Little), ) val xvm = parseXvm( getAreaAsset(episode, areaVariant, AssetType.Texture).cursor(Endianness.Little), ).unwrap() - areaGeometryToObject3DAndSections(obj, xvm.textures, episode, areaVariant) + val (renderObj3d, sections) = areaGeometryToObject3DAndSections( + renderObj, + xvm.textures, + episode, + areaVariant, + ) + + val collisionObj = parseAreaCollisionGeometry( + getAreaAsset(episode, areaVariant, AssetType.Collision) + .cursor(Endianness.Little) + ) + val collisionObj3d = + areaCollisionGeometryToObject3D(collisionObj, episode, areaVariant) + + addSectionsToCollisionGeometry(collisionObj3d, renderObj3d) + +// cullRenderGeometry(collisionObj3d, renderObj3d) + + Geom(sections, renderObj3d, collisionObj3d) }, - { (obj3d) -> disposeObject3DResources(obj3d) }, - ) - ) - - private val collisionObjectCache = addDisposable( - LoadingCache( - { key -> - val (episode, areaVariant) = key - val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision) - val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little)) - val obj3d = areaCollisionGeometryToObject3D(obj, episode, areaVariant) - - val (renderObj3d) = renderObjectCache.get(key) - addSectionsToCollisionGeometry(obj3d, renderObj3d) - - obj3d + { geom -> + disposeObject3DResources(geom.renderGeometry) + disposeObject3DResources(geom.collisionGeometry) }, - ::disposeObject3DResources, ) ) suspend fun loadSections(episode: Episode, areaVariant: AreaVariantModel): List = - renderObjectCache.get(EpisodeAndAreaVariant(episode, areaVariant)).second + cache.get(EpisodeAndAreaVariant(episode, areaVariant)).sections suspend fun loadRenderGeometry(episode: Episode, areaVariant: AreaVariantModel): Object3D = - renderObjectCache.get(EpisodeAndAreaVariant(episode, areaVariant)).first + cache.get(EpisodeAndAreaVariant(episode, areaVariant)).renderGeometry suspend fun loadCollisionGeometry( episode: Episode, areaVariant: AreaVariantModel, ): Object3D = - collisionObjectCache.get(EpisodeAndAreaVariant(episode, areaVariant)) + cache.get(EpisodeAndAreaVariant(episode, areaVariant)).collisionGeometry private suspend fun getAreaAsset( episode: Episode, @@ -91,8 +89,8 @@ 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).boundingBox!!.getCenter(tmpVec) + for (collisionMesh in collisionGeom.children) { + val origin = ((collisionMesh as Mesh).geometry).boundingBox!!.getCenter(tmpVec) // Cast a ray downward from the center of the section. raycaster.set(origin, DOWN) @@ -118,13 +116,52 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } if (intersection != null) { - val cud = collisionArea.userData.unsafeCast() + val cud = collisionMesh.userData.unsafeCast() val rud = intersection.`object`.userData.unsafeCast() cud.section = rud.section } } } + 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, areaVariant: AreaVariantModel, @@ -169,55 +206,34 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } private fun areaGeometryToObject3DAndSections( - renderObject: RenderObject, + renderGeometry: RenderGeometry, textures: List, episode: Episode, areaVariant: AreaVariantModel, ): Pair> { - val sections = mutableListOf() - val group = Group() - val textureCache = mutableMapOf() + val sections = mutableMapOf() - for ((i, section) in renderObject.sections.withIndex()) { - val sectionModel = if (section.id >= 0) { - SectionModel( - section.id, - vec3ToThree(section.position), - vec3ToEuler(section.rotation), - areaVariant, - ).also(sections::add) - } else null - - for (obj in section.objects) { - val builder = MeshBuilder(textures, textureCache) - ninjaObjectToMeshBuilder(obj, builder) - - builder.defaultMaterial(MeshBasicMaterial(obj { - color = Color().setHSL((i % 7) / 7.0, 1.0, .5) - transparent = true - opacity = .25 - side = DoubleSide - })) - - val mesh = builder.buildMesh() - - if (shouldRenderOnTop(obj, episode, areaVariant)) { + val group = + renderGeometryToGroup(renderGeometry, textures) { renderSection, xjObject, mesh -> + if (shouldRenderOnTop(xjObject, episode, areaVariant)) { mesh.renderOrder = 1 } - mesh.position.setFromVec3(section.position) - mesh.rotation.setFromVec3(section.rotation) - mesh.updateMatrixWorld() + if (renderSection.id >= 0) { + val sectionModel = sections.getOrPut(renderSection.id) { + SectionModel( + renderSection.id, + vec3ToThree(renderSection.position), + vec3ToEuler(renderSection.rotation), + areaVariant, + ) + } - sectionModel?.let { (mesh.userData.unsafeCast()).section = sectionModel } - - group.add(mesh) } - } - return Pair(group, sections) + return Pair(group, sections.values.toList()) } private fun shouldRenderOnTop( @@ -225,37 +241,14 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine episode: Episode, areaVariant: AreaVariantModel, ): Boolean { - // Manual fixes for various areas. Might not be necessary anymore once order-independent - // rendering is implemented. - val textureIds: Set = when { - // Pioneer 2 - episode == Episode.I && areaVariant.area.id == 0 -> - setOf(70, 71, 72, 126, 127, 155, 156, 198, 230, 231, 232, 233, 234) - // Forest 1 - episode == Episode.I && areaVariant.area.id == 1 -> - setOf(12, 41) - // Mine 2 - episode == Episode.I && areaVariant.area.id == 7 -> - setOf(0, 1, 7, 8, 17, 23, 56, 57, 58, 59, 60, 83) - // Ruins 1 - episode == Episode.I && areaVariant.area.id == 8 -> - setOf(1, 21, 22, 27, 28, 43, 51, 59, 70, 72, 75) - // Lab - episode == Episode.II && areaVariant.area.id == 0 -> - setOf(36, 37, 38, 48, 60, 67, 79, 80) - // Central Control Area - episode == Episode.II && areaVariant.area.id == 5 -> - (0..59).toSet() + setOf(69, 77) - else -> - return false - } - fun recurse(obj: XjObject): Boolean { obj.model?.meshes?.let { meshes -> for (mesh in meshes) { - mesh.material.textureId?.let { - if (it in textureIds) { - return true + mesh.material.textureId?.let { textureId -> + RENDER_ON_TOP_TEXTURES[Pair(episode, areaVariant.id)]?.let { textureIds -> + if (textureId in textureIds) { + return true + } } } } @@ -268,80 +261,20 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine } private fun areaCollisionGeometryToObject3D( - obj: CollisionObject, + obj: CollisionGeometry, episode: Episode, areaVariant: AreaVariantModel, ): Object3D { - val group = Group() - group.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}" - - for (collisionMesh in obj.meshes) { - val positions = jsArrayOf() - val normals = jsArrayOf() - val materialGroups = mutableMapOf>() - var index: Short = 0 - - for (triangle in collisionMesh.triangles) { - val isSectionTransition = (triangle.flags and 0b1000000) != 0 - val isVegetation = (triangle.flags and 0b10000) != 0 - val isGround = (triangle.flags and 0b1) != 0 - val materialIndex = when { - isSectionTransition -> 3 - isVegetation -> 2 - isGround -> 1 - else -> 0 - } - - // Filter out walls. - if (materialIndex != 0) { - 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 (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() - - group.add( - Mesh(geom, COLLISION_MATERIALS).apply { - renderOrder = 1 - } - ) - - group.add( - Mesh(geom, COLLISION_WIREFRAME_MATERIALS).apply { - renderOrder = 2 - } - ) + val group = collisionGeometryToGroup(obj) { + // Filter out walls and steep triangles. + if (it.flags.isBitSet(0) || it.flags.isBitSet(4) || it.flags.isBitSet(6)) { + tmpVec.setFromVec3(it.normal) + tmpVec dot UP >= COS_75_DEG + } else { + false } } - + group.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}" return group } @@ -350,63 +283,21 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine val areaVariant: AreaVariantModel, ) + private class Geom( + val sections: List, + val renderGeometry: Object3D, + val collisionGeometry: Object3D, + ) + private enum class AssetType { Render, Collision, Texture } companion object { + private val COS_75_DEG = cos(PI / 180 * 75) private val DOWN = Vector3(.0, -1.0, .0) private val UP = Vector3(.0, 1.0, .0) - private val COLLISION_MATERIALS: Array = arrayOf( - // Wall - MeshBasicMaterial(obj { - color = Color(0x80c0d0) - transparent = true - opacity = .25 - }), - // Ground - MeshLambertMaterial(obj { - color = Color(0x405050) - side = DoubleSide - }), - // Vegetation - MeshLambertMaterial(obj { - color = Color(0x306040) - side = DoubleSide - }), - // Section transition zone - MeshLambertMaterial(obj { - color = Color(0x402050) - side = DoubleSide - }), - ) - - private val COLLISION_WIREFRAME_MATERIALS: Array = arrayOf( - // Wall - MeshBasicMaterial(obj { - color = Color(0x90d0e0) - wireframe = true - transparent = true - opacity = .3 - }), - // Ground - MeshBasicMaterial(obj { - color = Color(0x506060) - wireframe = true - }), - // Vegetation - MeshBasicMaterial(obj { - color = Color(0x405050) - wireframe = true - }), - // Section transition zone - MeshBasicMaterial(obj { - color = Color(0x503060) - wireframe = true - }), - ) - private val AREA_BASE_NAMES: Map>> = mapOf( Episode.I to listOf( Pair("city00", true), @@ -459,6 +350,28 @@ 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. + */ + val RENDER_ON_TOP_TEXTURES: Map, Set> = mapOf( + // Pioneer 2 + Pair(Episode.I, 0) to setOf( + 70, 71, 72, 126, 127, 155, 156, 198, 230, 231, 232, 233, 234, + ), + // Forest 1 + Pair(Episode.I, 1) to setOf(12, 41), + // Mine 2 + Pair(Episode.I, 7) to setOf(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), + // Lab + Pair(Episode.II, 0) to setOf(36, 37, 38, 48, 60, 67, 79, 80), + // Central Control Area + Pair(Episode.II, 5) to (0..59).toSet() + setOf(69, 77), + ) + private val raycaster = Raycaster() private val tmpVec = Vector3() private val tmpIntersections = arrayOf() 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 c1a76e30..65ab50a5 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 @@ -9,8 +9,11 @@ import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.fileFormats.ninja.* import world.phantasmal.lib.fileFormats.parseAfs +import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry +import world.phantasmal.lib.fileFormats.parseAreaRenderGeometry import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal +import world.phantasmal.web.viewer.stores.NinjaGeometry import world.phantasmal.web.viewer.stores.ViewerStore import world.phantasmal.webui.controllers.Controller import world.phantasmal.webui.extension @@ -62,7 +65,7 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { var success = false try { - var ninjaObject: NinjaObject<*, *>? = null + var ninjaGeometry: NinjaGeometry? = null var textures: List? = null var ninjaMotion: NjMotion? = null @@ -78,7 +81,7 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { fileResult = njResult if (njResult is Success) { - ninjaObject = njResult.value.firstOrNull() + ninjaGeometry = njResult.value.firstOrNull()?.let(NinjaGeometry::Object) } } @@ -87,7 +90,19 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { fileResult = xjResult if (xjResult is Success) { - ninjaObject = xjResult.value.firstOrNull() + ninjaGeometry = xjResult.value.firstOrNull()?.let(NinjaGeometry::Object) + } + } + + "rel" -> { + if (file.name.endsWith("c.rel")) { + val collisionGeometry = parseAreaCollisionGeometry(cursor) + fileResult = Success(collisionGeometry) + ninjaGeometry = NinjaGeometry.Collision(collisionGeometry) + } else { + val renderGeometry = parseAreaRenderGeometry(cursor) + fileResult = Success(renderGeometry) + ninjaGeometry = NinjaGeometry.Render(renderGeometry) } } @@ -131,7 +146,7 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { } } - ninjaObject?.let(store::setCurrentNinjaObject) + ninjaGeometry?.let(store::setCurrentNinjaGeometry) textures?.let(store::setCurrentTextures) ninjaMotion?.let(store::setCurrentNinjaMotion) } catch (e: Exception) { 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 5372a5af..a75ca5cb 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 @@ -31,7 +31,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab val texIds = textureIds(char, sectionId, body) return listOf( - texIds.section_id, + texIds.sectionId, *texIds.body, *texIds.head, *texIds.hair, @@ -128,7 +128,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab HUmar -> { val bodyIdx = body * 3 TextureIds( - section_id = sectionId.ordinal + 126, + sectionId = sectionId.ordinal + 126, body = arrayOf(bodyIdx, bodyIdx + 1, bodyIdx + 2, body + 108), head = arrayOf(54, 55), hair = arrayOf(94, 95), @@ -138,7 +138,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab HUnewearl -> { val bodyIdx = body * 13 TextureIds( - section_id = sectionId.ordinal + 299, + sectionId = sectionId.ordinal + 299, body = arrayOf( bodyIdx + 13, bodyIdx, @@ -156,7 +156,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab HUcast -> { val bodyIdx = body * 5 TextureIds( - section_id = sectionId.ordinal + 275, + sectionId = sectionId.ordinal + 275, body = arrayOf(bodyIdx, bodyIdx + 1, bodyIdx + 2, body + 250), head = arrayOf(bodyIdx + 3, bodyIdx + 4), hair = arrayOf(), @@ -166,7 +166,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab HUcaseal -> { val bodyIdx = body * 5 TextureIds( - section_id = sectionId.ordinal + 375, + sectionId = sectionId.ordinal + 375, body = arrayOf(bodyIdx, bodyIdx + 1, bodyIdx + 2), head = arrayOf(bodyIdx + 3, bodyIdx + 4), hair = arrayOf(), @@ -176,7 +176,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab RAmar -> { val bodyIdx = body * 7 TextureIds( - section_id = sectionId.ordinal + 197, + sectionId = sectionId.ordinal + 197, body = arrayOf(bodyIdx + 4, bodyIdx + 5, bodyIdx + 6, body + 179), head = arrayOf(126, 127), hair = arrayOf(166, 167), @@ -186,7 +186,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab RAmarl -> { val bodyIdx = body * 16 TextureIds( - section_id = sectionId.ordinal + 322, + sectionId = sectionId.ordinal + 322, body = arrayOf(bodyIdx + 15, bodyIdx + 1, bodyIdx), head = arrayOf(288), hair = arrayOf(308, 309), @@ -196,7 +196,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab RAcast -> { val bodyIdx = body * 5 TextureIds( - section_id = sectionId.ordinal + 300, + sectionId = sectionId.ordinal + 300, body = arrayOf(bodyIdx, bodyIdx + 1, bodyIdx + 2, bodyIdx + 3, body + 275), head = arrayOf(bodyIdx + 4), hair = arrayOf(), @@ -206,7 +206,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab RAcaseal -> { val bodyIdx = body * 5 TextureIds( - section_id = sectionId.ordinal + 375, + sectionId = sectionId.ordinal + 375, body = arrayOf(body + 350, bodyIdx, bodyIdx + 1, bodyIdx + 2), head = arrayOf(bodyIdx + 3), hair = arrayOf(bodyIdx + 4), @@ -216,7 +216,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab FOmar -> { val bodyIdx = if (body == 0) 0 else body * 15 + 2 TextureIds( - section_id = sectionId.ordinal + 310, + sectionId = sectionId.ordinal + 310, body = arrayOf(bodyIdx + 12, bodyIdx + 13, bodyIdx + 14, bodyIdx), head = arrayOf(276, 272), hair = arrayOf(null, 296, 297), @@ -226,7 +226,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab FOmarl -> { val bodyIdx = body * 16 TextureIds( - section_id = sectionId.ordinal + 326, + sectionId = sectionId.ordinal + 326, body = arrayOf(bodyIdx, bodyIdx + 2, bodyIdx + 1, 322 /*hands*/), head = arrayOf(288), hair = arrayOf(null, null, 308), @@ -236,7 +236,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab FOnewm -> { val bodyIdx = body * 17 TextureIds( - section_id = sectionId.ordinal + 344, + sectionId = sectionId.ordinal + 344, body = arrayOf(bodyIdx + 4, 340 /*hands*/, bodyIdx, bodyIdx + 5), head = arrayOf(306, 310), hair = arrayOf(null, null, 330), @@ -247,7 +247,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab FOnewearl -> { val bodyIdx = body * 26 TextureIds( - section_id = sectionId.ordinal + 505, + sectionId = sectionId.ordinal + 505, body = arrayOf(bodyIdx + 1, bodyIdx, bodyIdx + 2, 501 /*hands*/), head = arrayOf(472, 468), hair = arrayOf(null, null, 492), @@ -257,7 +257,7 @@ class CharacterClassAssetLoader(private val assetLoader: AssetLoader) : Disposab } private class TextureIds( - val section_id: Int, + val sectionId: Int, val body: Array, val head: Array, val hair: Array, 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 ebfb8dba..e6f4f30c 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 @@ -6,14 +6,14 @@ 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.boundingSphere +import world.phantasmal.web.core.isSkinnedMesh 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.rendering.conversion.* import world.phantasmal.web.core.times import world.phantasmal.web.externals.three.* +import world.phantasmal.web.viewer.stores.NinjaGeometry import world.phantasmal.web.viewer.stores.ViewerStore import kotlin.math.roundToInt import kotlin.math.tan @@ -24,7 +24,7 @@ class MeshRenderer( ) : Renderer() { private val clock = Clock() - private var mesh: Mesh? = null + private var obj3d: Object3D? = null private var skeletonHelper: SkeletonHelper? = null private var animation: Animation? = null private var updateAnimationTime = true @@ -50,7 +50,7 @@ class MeshRenderer( )) init { - observe(viewerStore.currentNinjaObject) { ninjaObjectOrXvmChanged() } + observe(viewerStore.currentNinjaGeometry) { ninjaObjectOrXvmChanged() } observe(viewerStore.currentTextures) { ninjaObjectOrXvmChanged() } observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged) observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it } @@ -82,7 +82,7 @@ class MeshRenderer( private fun ninjaObjectOrXvmChanged() { // Remove the previous mesh. - mesh?.let { mesh -> + obj3d?.let { mesh -> disposeObject3DResources(mesh) context.scene.remove(mesh) } @@ -93,7 +93,7 @@ class MeshRenderer( skeletonHelper = null } - val ninjaObject = viewerStore.currentNinjaObject.value + val ninjaGeometry = viewerStore.currentNinjaGeometry.value val textures = viewerStore.currentTextures.value // Stop and clean up previous animation and store animation time. @@ -106,14 +106,23 @@ class MeshRenderer( } // Create a new mesh if necessary. - if (ninjaObject != null) { - val mesh = - if (ninjaObject is NjObject) { - ninjaObjectToSkinnedMesh(ninjaObject, textures, boundingVolumes = true) - } else { - ninjaObjectToMesh(ninjaObject, textures, boundingVolumes = true) + if (ninjaGeometry != null) { + val obj3d = when (ninjaGeometry) { + is NinjaGeometry.Object -> { + val obj = ninjaGeometry.obj + + if (obj is NjObject) { + ninjaObjectToSkinnedMesh(obj, textures, boundingVolumes = true) + } else { + ninjaObjectToMesh(obj, textures, boundingVolumes = true) + } } + is NinjaGeometry.Render -> renderGeometryToGroup(ninjaGeometry.geometry, textures) + + is NinjaGeometry.Collision -> collisionGeometryToGroup(ninjaGeometry.geometry) + } + // 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. val charClassActive = viewerStore.currentCharacterClass.value != null @@ -122,19 +131,19 @@ class MeshRenderer( if (resetCamera) { // Compute camera position. - val bSphere = mesh.geometry.boundingSphere!! + val bSphere = boundingSphere(obj3d) val cameraDistFactor = 1.5 / tan(degToRad((context.camera as PerspectiveCamera).fov) / 2) val cameraPos = CAMERA_POS * (bSphere.radius * cameraDistFactor) inputManager.lookAt(cameraPos, bSphere.center) } - context.scene.add(mesh) - this.mesh = mesh + context.scene.add(obj3d) + this.obj3d = obj3d - if (mesh is SkinnedMesh) { + if (obj3d.isSkinnedMesh() && ninjaGeometry is NinjaGeometry.Object) { // Add skeleton. - val skeletonHelper = SkeletonHelper(mesh) + val skeletonHelper = SkeletonHelper(obj3d) skeletonHelper.visible = viewerStore.showSkeleton.value skeletonHelper.asDynamic().material.lineWidth = 3 @@ -143,7 +152,7 @@ class MeshRenderer( // Create a new animation mixer and clip. viewerStore.currentNinjaMotion.value?.let { njMotion -> - animation = Animation(ninjaObject, njMotion, mesh).also { + animation = Animation(ninjaGeometry.obj, njMotion, obj3d).also { it.mixer.timeScale = viewerStore.frameRate.value / PSO_FRAME_RATE_DOUBLE it.action.time = animationTime ?: .0 it.action.play() @@ -159,10 +168,10 @@ class MeshRenderer( animation = null } - val mesh = mesh - val njObject = viewerStore.currentNinjaObject.value + val mesh = obj3d + val njObject = (viewerStore.currentNinjaGeometry.value as? NinjaGeometry.Object)?.obj - if (mesh == null || mesh !is SkinnedMesh || njObject == null || njMotion == null) { + if (mesh == null || !mesh.isSkinnedMesh() || njObject == null || njMotion == null) { return } 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 a3bfdff2..a93ce0b7 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,6 +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.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.XvrTexture @@ -23,13 +25,19 @@ import world.phantasmal.webui.stores.Store private val logger = KotlinLogging.logger {} +sealed class NinjaGeometry { + class Object(val obj: NinjaObject<*, *>) : NinjaGeometry() + class Render(val geometry: RenderGeometry) : NinjaGeometry() + class Collision(val geometry: CollisionGeometry) : NinjaGeometry() +} + class ViewerStore( private val characterClassAssetLoader: CharacterClassAssetLoader, private val animationAssetLoader: AnimationAssetLoader, uiStore: UiStore, ) : Store() { // Ninja concepts. - private val _currentNinjaObject = mutableVal?>(null) + private val _currentNinjaGeometry = mutableVal(null) private val _currentTextures = mutableListVal() private val _currentNinjaMotion = mutableVal(null) @@ -47,7 +55,7 @@ class ViewerStore( private val _frame = mutableVal(0) // Ninja concepts. - val currentNinjaObject: Val?> = _currentNinjaObject + val currentNinjaGeometry: Val = _currentNinjaGeometry val currentTextures: ListVal = _currentTextures val currentNinjaMotion: Val = _currentNinjaMotion @@ -58,7 +66,7 @@ class ViewerStore( val animations: List = (0 until 572).map { AnimationModel( "Animation ${it + 1}", - "/player/animation/animation_${it.toString().padStart(3, '0')}.njm" + "/player/animation/animation_${it.toString().padStart(3, '0')}.njm", ) } val currentAnimation: Val = _currentAnimation @@ -143,7 +151,7 @@ class ViewerStore( } } - fun setCurrentNinjaObject(ninjaObject: NinjaObject<*, *>?) { + fun setCurrentNinjaGeometry(geometry: NinjaGeometry?) { if (_currentCharacterClass.value != null) { _currentCharacterClass.value = null _currentTextures.clear() @@ -151,7 +159,7 @@ class ViewerStore( _currentAnimation.value = null _currentNinjaMotion.value = null - _currentNinjaObject.value = ninjaObject + _currentNinjaGeometry.value = geometry } fun setCurrentTextures(textures: List) { @@ -233,14 +241,14 @@ class ViewerStore( _currentNinjaMotion.value = null } - _currentNinjaObject.value = ninjaObject + _currentNinjaGeometry.value = NinjaGeometry.Object(ninjaObject) _currentTextures.replaceAll(textures) } catch (e: Exception) { logger.error(e) { "Couldn't load Ninja model for $char." } _currentAnimation.value = null _currentNinjaMotion.value = null - _currentNinjaObject.value = null + _currentNinjaGeometry.value = null _currentTextures.clear() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerToolbar.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerToolbar.kt index 0c419179..36237312 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerToolbar.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerToolbar.kt @@ -17,7 +17,7 @@ class ViewerToolbar(private val ctrl: ViewerToolbarController) : Widget() { FileButton( text = "Open file...", iconLeft = Icon.File, - accept = ".afs, .nj, .njm, .xj, .xvm", + accept = ".afs, .nj, .njm, .rel, .xj, .xvm", multiple = true, filesSelected = { files -> scope.launch { ctrl.openFiles(files) } }, ), diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt index 187ec9c3..662bc9e8 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt @@ -72,7 +72,7 @@ class Toolbar( } .pw-toolbar .pw-input { - height: 26px; + height: 24px; } """.trimIndent()) }