mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
Upgraded to ThreeJS r127. The viewer can now load n.rel and c.rel geometry files.
This commit is contained in:
parent
60d0bc6116
commit
5be29df0ac
21
FEATURES.md
21
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
|
||||
|
@ -2,7 +2,7 @@ package world.phantasmal.lib.fileFormats
|
||||
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
|
||||
class CollisionObject(
|
||||
class CollisionGeometry(
|
||||
val meshes: List<CollisionMesh>,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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<RenderSection>,
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ class RenderSection(
|
||||
val objects: List<XjObject>,
|
||||
)
|
||||
|
||||
fun parseAreaGeometry(cursor: Cursor): RenderObject {
|
||||
fun parseAreaRenderGeometry(cursor: Cursor): RenderGeometry {
|
||||
val sections = mutableListOf<RenderSection>()
|
||||
|
||||
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.
|
@ -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"))
|
||||
|
@ -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<Mesh>().isMesh
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun Object3D.isSkinnedMesh(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@isSkinnedMesh is SkinnedMesh)
|
||||
}
|
||||
|
||||
return unsafeCast<SkinnedMesh>().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
|
||||
}
|
||||
|
@ -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<Material> = 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<Material> = 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<XvrTexture?>,
|
||||
processMesh: (RenderSection, XjObject, Mesh) -> Unit = { _, _, _ -> },
|
||||
): Group {
|
||||
val group = Group()
|
||||
val textureCache = mutableMapOf<Int, Texture?>()
|
||||
|
||||
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<Float>()
|
||||
val normals = jsArrayOf<Float>()
|
||||
val materialGroups = mutableMapOf<Int, JsArray<Short>>()
|
||||
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()
|
||||
|
@ -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<Material>,
|
||||
)
|
||||
|
||||
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 */
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<EpisodeAndAreaVariant, Pair<Object3D, List<SectionModel>>>(
|
||||
private val cache = addDisposable(
|
||||
LoadingCache<EpisodeAndAreaVariant, Geom>(
|
||||
{ (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<EpisodeAndAreaVariant, Object3D>(
|
||||
{ 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<SectionModel> =
|
||||
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<AreaUserData>()
|
||||
val cud = collisionMesh.userData.unsafeCast<AreaUserData>()
|
||||
val rud = intersection.`object`.userData.unsafeCast<AreaUserData>()
|
||||
cud.section = rud.section
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cullRenderGeometry(collisionGeom: Object3D, renderGeom: Object3D) {
|
||||
val cullingVolumes = mutableMapOf<Int, Box3>()
|
||||
|
||||
for (collisionMesh in collisionGeom.children) {
|
||||
collisionMesh as Mesh
|
||||
collisionMesh.userData.unsafeCast<AreaUserData>().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<XvrTexture>,
|
||||
episode: Episode,
|
||||
areaVariant: AreaVariantModel,
|
||||
): Pair<Object3D, List<SectionModel>> {
|
||||
val sections = mutableListOf<SectionModel>()
|
||||
val group = Group()
|
||||
val textureCache = mutableMapOf<Int, Texture?>()
|
||||
val sections = mutableMapOf<Int, SectionModel>()
|
||||
|
||||
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<AreaUserData>()).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<Int> = 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<Float>()
|
||||
val normals = jsArrayOf<Float>()
|
||||
val materialGroups = mutableMapOf<Int, JsArray<Short>>()
|
||||
var index: Short = 0
|
||||
|
||||
for (triangle in collisionMesh.triangles) {
|
||||
val isSectionTransition = (triangle.flags and 0b1000000) != 0
|
||||
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<SectionModel>,
|
||||
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<Material> = 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<Material> = 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<Episode, List<Pair<String, Boolean>>> = 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<Pair<Episode, Int>, Set<Int>> = 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<Intersection>()
|
||||
|
@ -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<XvrTexture>? = 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) {
|
||||
|
@ -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<Int>,
|
||||
val head: Array<Int>,
|
||||
val hair: Array<Int?>,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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<NinjaObject<*, *>?>(null)
|
||||
private val _currentNinjaGeometry = mutableVal<NinjaGeometry?>(null)
|
||||
private val _currentTextures = mutableListVal<XvrTexture?>()
|
||||
private val _currentNinjaMotion = mutableVal<NjMotion?>(null)
|
||||
|
||||
@ -47,7 +55,7 @@ class ViewerStore(
|
||||
private val _frame = mutableVal(0)
|
||||
|
||||
// Ninja concepts.
|
||||
val currentNinjaObject: Val<NinjaObject<*, *>?> = _currentNinjaObject
|
||||
val currentNinjaGeometry: Val<NinjaGeometry?> = _currentNinjaGeometry
|
||||
val currentTextures: ListVal<XvrTexture?> = _currentTextures
|
||||
val currentNinjaMotion: Val<NjMotion?> = _currentNinjaMotion
|
||||
|
||||
@ -58,7 +66,7 @@ class ViewerStore(
|
||||
val animations: List<AnimationModel> = (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<AnimationModel?> = _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<XvrTexture>) {
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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) } },
|
||||
),
|
||||
|
@ -72,7 +72,7 @@ class Toolbar(
|
||||
}
|
||||
|
||||
.pw-toolbar .pw-input {
|
||||
height: 26px;
|
||||
height: 24px;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user