From 6003797f1571b17f2455d1c32f9ee8997cc9e042 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 9 Apr 2021 20:56:51 +0200 Subject: [PATCH] Textures can now be turned off in the viewer. --- .../world/phantasmal/web/core/Throttle.kt | 24 +++ .../conversion/NinjaGeometryConversion.kt | 4 +- .../phantasmal/web/externals/three/three.kt | 2 +- .../controllers/ViewerToolbarController.kt | 6 + .../web/viewer/rendering/MeshRenderer.kt | 138 +++++++++--------- .../web/viewer/stores/ViewerStore.kt | 9 ++ .../web/viewer/widgets/ViewerToolbar.kt | 6 + 7 files changed, 121 insertions(+), 68 deletions(-) create mode 100644 web/src/main/kotlin/world/phantasmal/web/core/Throttle.kt diff --git a/web/src/main/kotlin/world/phantasmal/web/core/Throttle.kt b/web/src/main/kotlin/world/phantasmal/web/core/Throttle.kt new file mode 100644 index 00000000..ccade698 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/core/Throttle.kt @@ -0,0 +1,24 @@ +package world.phantasmal.web.core + +import kotlinx.browser.window + +/** + * Helper for limiting the amount of times a function is called within a given time frame. + * + * @param before the amount of time in ms before the function is actually called. E.g., if [before] + * is 10 and [invoke] is called once, the given function won't be called until 10ms have passed. If + * invoke is called again before the 10ms have passed, it will still only be called once after 10ms + * have passed since the first call to invoke. + */ +class Throttle(private val before: Int) { + private var timeout: Int = -1 + + operator fun invoke(f: () -> Unit) { + if (timeout == -1) { + timeout = window.setTimeout({ + f() + timeout = -1 + }, before) + } + } +} 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 62b2b2a4..c97bbfab 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 @@ -164,10 +164,10 @@ private fun xjObjectToMesh( val builder = MeshBuilder(textures, textureCache) ninjaObjectToMeshBuilder(xjObj, builder) - builder.defaultMaterial(MeshBasicMaterial(obj { + builder.defaultMaterial(MeshLambertMaterial(obj { color = Color().setHSL((index % 7) / 7.0, 1.0, .5) transparent = true - opacity = .25 + opacity = .5 side = DoubleSide })) 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 2a33842d..501884cc 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 @@ -579,6 +579,7 @@ external interface MaterialParameters { var blending: Blending var side: Side var transparent: Boolean + var opacity: Double } open external class Material : EventDispatcher { @@ -592,7 +593,6 @@ open external class Material : EventDispatcher { external interface MeshBasicMaterialParameters : MaterialParameters { var color: Color - var opacity: Double var map: Texture? var wireframe: Boolean var wireframeLinewidth: Double 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 fc4fb75a..e88d6ba5 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 @@ -28,6 +28,8 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { private val _resultDialogVisible = mutableVal(false) private val _result = mutableVal?>(null) + val applyTexturesEnabled: Val = store.applyTexturesEnabled + val applyTextures: Val = store.applyTextures val showSkeletonEnabled: Val = store.showSkeletonEnabled val showSkeleton: Val = store.showSkeleton val playAnimation: Val = store.animationPlaying @@ -44,6 +46,10 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() { } } + fun setApplyTextures(apply: Boolean) { + store.setApplyTextures(apply) + } + fun setShowSkeleton(show: Boolean) { store.setShowSkeleton(show) } 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 e04590d5..c72f406a 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,6 +6,7 @@ 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.Throttle import world.phantasmal.web.core.boundingSphere import world.phantasmal.web.core.isSkinnedMesh import world.phantasmal.web.core.rendering.* @@ -23,6 +24,7 @@ class MeshRenderer( createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer, ) : Renderer() { private val clock = Clock() + private val throttleRebuildMesh = Throttle(before = 10) private var obj3d: Object3D? = null private var skeletonHelper: SkeletonHelper? = null @@ -45,13 +47,14 @@ class MeshRenderer( override val inputManager = addDisposable(OrbitalCameraInputManager( context.canvas, context.camera, - Vector3(), + position = Vector3(.0, .0, .0), screenSpacePanning = true, )) init { - observe(viewerStore.currentNinjaGeometry) { ninjaGeometryOrXvmChanged() } - observe(viewerStore.currentTextures) { ninjaGeometryOrXvmChanged() } + observe(viewerStore.currentNinjaGeometry) { rebuildMesh(resetCamera = true) } + observe(viewerStore.currentTextures) { rebuildMesh(resetCamera = true) } + observe(viewerStore.applyTextures) { rebuildMesh(resetCamera = false) } observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged) observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it } observe(viewerStore.animationPlaying, ::animationPlayingChanged) @@ -80,82 +83,87 @@ class MeshRenderer( } } - private fun ninjaGeometryOrXvmChanged() { - // Remove the previous mesh. - obj3d?.let { mesh -> - disposeObject3DResources(mesh) - context.scene.remove(mesh) - } + private fun rebuildMesh(resetCamera: Boolean) { + throttleRebuildMesh { + // Remove the previous mesh. + obj3d?.let { mesh -> + disposeObject3DResources(mesh) + context.scene.remove(mesh) + } - // Remove the previous skeleton. - skeletonHelper?.let { - context.scene.remove(it) - skeletonHelper = null - } + // Remove the previous skeleton. + skeletonHelper?.let { + context.scene.remove(it) + skeletonHelper = null + } - val ninjaGeometry = viewerStore.currentNinjaGeometry.value - val textures = viewerStore.currentTextures.value + val ninjaGeometry = viewerStore.currentNinjaGeometry.value + val textures = + if (viewerStore.applyTextures.value) viewerStore.currentTextures.value + else emptyList() - // Stop and clean up previous animation and store animation time. - var animationTime: Double? = null + // Stop and clean up previous animation and store animation time. + var animationTime: Double? = null - animation?.let { - animationTime = it.action.time - it.dispose() - this.animation = null - } + animation?.let { + animationTime = it.action.time + it.dispose() + this.animation = null + } - // Create a new mesh if necessary. - if (ninjaGeometry != null) { - val obj3d = when (ninjaGeometry) { - is NinjaGeometry.Object -> { - val obj = ninjaGeometry.obj + // Create a new mesh if necessary. + 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) + 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) } - is NinjaGeometry.Render -> renderGeometryToGroup(ninjaGeometry.geometry, textures) + // Determine whether camera needs to be reset. Resets should always happen when the + // Ninja geometry changes except when we're switching between character class models. + val charClassActive = viewerStore.currentCharacterClass.value != null + val cameraResetNecessary = !charClassActive || !this.charClassActive + this.charClassActive = charClassActive - is NinjaGeometry.Collision -> collisionGeometryToGroup(ninjaGeometry.geometry) - } + if (resetCamera && cameraResetNecessary) { + // Compute camera position. + 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) + } - // Determine whether camera needs to be reset. Resets should always happen when the - // Ninja geometry changes except when we're switching between character class models. - val charClassActive = viewerStore.currentCharacterClass.value != null - val resetCamera = !charClassActive || !this.charClassActive - this.charClassActive = charClassActive + context.scene.add(obj3d) + this.obj3d = obj3d - if (resetCamera) { - // Compute camera position. - 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) - } + if (obj3d.isSkinnedMesh() && ninjaGeometry is NinjaGeometry.Object) { + // Add skeleton. + val skeletonHelper = SkeletonHelper(obj3d) + skeletonHelper.visible = viewerStore.showSkeleton.value + skeletonHelper.material.unsafeCast().linewidth = 3 - context.scene.add(obj3d) - this.obj3d = obj3d + context.scene.add(skeletonHelper) + this.skeletonHelper = skeletonHelper - if (obj3d.isSkinnedMesh() && ninjaGeometry is NinjaGeometry.Object) { - // Add skeleton. - val skeletonHelper = SkeletonHelper(obj3d) - skeletonHelper.visible = viewerStore.showSkeleton.value - skeletonHelper.material.unsafeCast().linewidth = 3 - - context.scene.add(skeletonHelper) - this.skeletonHelper = skeletonHelper - - // Create a new animation mixer and clip. - viewerStore.currentNinjaMotion.value?.let { njMotion -> - 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() + // Create a new animation mixer and clip. + viewerStore.currentNinjaMotion.value?.let { njMotion -> + 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() + } } } } 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 1dbdfd03..e26812bb 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 @@ -51,6 +51,7 @@ class ViewerStore( private val _currentAnimation = mutableVal(null) // Settings. + private val _applyTextures = mutableVal(true) private val _showSkeleton = mutableVal(false) private val _animationPlaying = mutableVal(true) private val _frameRate = mutableVal(PSO_FRAME_RATE) @@ -74,6 +75,10 @@ class ViewerStore( val currentAnimation: Val = _currentAnimation // Settings. + val applyTexturesEnabled: Val = _currentNinjaGeometry.map { + it == null || it !is NinjaGeometry.Collision + } + val applyTextures: Val = applyTexturesEnabled and _applyTextures val showSkeletonEnabled: Val = _currentNinjaGeometry.map { it is NinjaGeometry.Object && it.obj is NjObject } @@ -208,6 +213,10 @@ class ViewerStore( } } + fun setApplyTextures(apply: Boolean) { + _applyTextures.value = apply + } + fun setShowSkeleton(show: Boolean) { _showSkeleton.value = show } 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 b6c3dedc..04dcf717 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 @@ -27,6 +27,12 @@ class ViewerToolbar(private val ctrl: ViewerToolbarController) : Widget() { checked = ctrl.showSkeleton, onChange = ctrl::setShowSkeleton, ), + Checkbox( + label = "Apply textures", + enabled = ctrl.applyTexturesEnabled, + checked = ctrl.applyTextures, + onChange = ctrl::setApplyTextures, + ), Checkbox( label = "Play animation", enabled = ctrl.animationControlsEnabled,