Textures can now be turned off in the viewer.

This commit is contained in:
Daan Vanden Bosch 2021-04-09 20:56:51 +02:00
parent 92017ca8ec
commit 6003797f15
7 changed files with 121 additions and 68 deletions

View File

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

View File

@ -164,10 +164,10 @@ private fun xjObjectToMesh(
val builder = MeshBuilder(textures, textureCache) val builder = MeshBuilder(textures, textureCache)
ninjaObjectToMeshBuilder(xjObj, builder) ninjaObjectToMeshBuilder(xjObj, builder)
builder.defaultMaterial(MeshBasicMaterial(obj { builder.defaultMaterial(MeshLambertMaterial(obj {
color = Color().setHSL((index % 7) / 7.0, 1.0, .5) color = Color().setHSL((index % 7) / 7.0, 1.0, .5)
transparent = true transparent = true
opacity = .25 opacity = .5
side = DoubleSide side = DoubleSide
})) }))

View File

@ -579,6 +579,7 @@ external interface MaterialParameters {
var blending: Blending var blending: Blending
var side: Side var side: Side
var transparent: Boolean var transparent: Boolean
var opacity: Double
} }
open external class Material : EventDispatcher { open external class Material : EventDispatcher {
@ -592,7 +593,6 @@ open external class Material : EventDispatcher {
external interface MeshBasicMaterialParameters : MaterialParameters { external interface MeshBasicMaterialParameters : MaterialParameters {
var color: Color var color: Color
var opacity: Double
var map: Texture? var map: Texture?
var wireframe: Boolean var wireframe: Boolean
var wireframeLinewidth: Double var wireframeLinewidth: Double

View File

@ -28,6 +28,8 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
private val _resultDialogVisible = mutableVal(false) private val _resultDialogVisible = mutableVal(false)
private val _result = mutableVal<PwResult<*>?>(null) private val _result = mutableVal<PwResult<*>?>(null)
val applyTexturesEnabled: Val<Boolean> = store.applyTexturesEnabled
val applyTextures: Val<Boolean> = store.applyTextures
val showSkeletonEnabled: Val<Boolean> = store.showSkeletonEnabled val showSkeletonEnabled: Val<Boolean> = store.showSkeletonEnabled
val showSkeleton: Val<Boolean> = store.showSkeleton val showSkeleton: Val<Boolean> = store.showSkeleton
val playAnimation: Val<Boolean> = store.animationPlaying val playAnimation: Val<Boolean> = store.animationPlaying
@ -44,6 +46,10 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
} }
} }
fun setApplyTextures(apply: Boolean) {
store.setApplyTextures(apply)
}
fun setShowSkeleton(show: Boolean) { fun setShowSkeleton(show: Boolean) {
store.setShowSkeleton(show) store.setShowSkeleton(show)
} }

View File

@ -6,6 +6,7 @@ import world.phantasmal.core.math.degToRad
import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NinjaObject
import world.phantasmal.lib.fileFormats.ninja.NjMotion import world.phantasmal.lib.fileFormats.ninja.NjMotion
import world.phantasmal.lib.fileFormats.ninja.NjObject import world.phantasmal.lib.fileFormats.ninja.NjObject
import world.phantasmal.web.core.Throttle
import world.phantasmal.web.core.boundingSphere import world.phantasmal.web.core.boundingSphere
import world.phantasmal.web.core.isSkinnedMesh import world.phantasmal.web.core.isSkinnedMesh
import world.phantasmal.web.core.rendering.* import world.phantasmal.web.core.rendering.*
@ -23,6 +24,7 @@ class MeshRenderer(
createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer, createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer,
) : Renderer() { ) : Renderer() {
private val clock = Clock() private val clock = Clock()
private val throttleRebuildMesh = Throttle(before = 10)
private var obj3d: Object3D? = null private var obj3d: Object3D? = null
private var skeletonHelper: SkeletonHelper? = null private var skeletonHelper: SkeletonHelper? = null
@ -45,13 +47,14 @@ class MeshRenderer(
override val inputManager = addDisposable(OrbitalCameraInputManager( override val inputManager = addDisposable(OrbitalCameraInputManager(
context.canvas, context.canvas,
context.camera, context.camera,
Vector3(), position = Vector3(.0, .0, .0),
screenSpacePanning = true, screenSpacePanning = true,
)) ))
init { init {
observe(viewerStore.currentNinjaGeometry) { ninjaGeometryOrXvmChanged() } observe(viewerStore.currentNinjaGeometry) { rebuildMesh(resetCamera = true) }
observe(viewerStore.currentTextures) { ninjaGeometryOrXvmChanged() } observe(viewerStore.currentTextures) { rebuildMesh(resetCamera = true) }
observe(viewerStore.applyTextures) { rebuildMesh(resetCamera = false) }
observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged) observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged)
observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it } observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it }
observe(viewerStore.animationPlaying, ::animationPlayingChanged) observe(viewerStore.animationPlaying, ::animationPlayingChanged)
@ -80,82 +83,87 @@ class MeshRenderer(
} }
} }
private fun ninjaGeometryOrXvmChanged() { private fun rebuildMesh(resetCamera: Boolean) {
// Remove the previous mesh. throttleRebuildMesh {
obj3d?.let { mesh -> // Remove the previous mesh.
disposeObject3DResources(mesh) obj3d?.let { mesh ->
context.scene.remove(mesh) disposeObject3DResources(mesh)
} context.scene.remove(mesh)
}
// Remove the previous skeleton. // Remove the previous skeleton.
skeletonHelper?.let { skeletonHelper?.let {
context.scene.remove(it) context.scene.remove(it)
skeletonHelper = null skeletonHelper = null
} }
val ninjaGeometry = viewerStore.currentNinjaGeometry.value val ninjaGeometry = viewerStore.currentNinjaGeometry.value
val textures = viewerStore.currentTextures.value val textures =
if (viewerStore.applyTextures.value) viewerStore.currentTextures.value
else emptyList()
// Stop and clean up previous animation and store animation time. // Stop and clean up previous animation and store animation time.
var animationTime: Double? = null var animationTime: Double? = null
animation?.let { animation?.let {
animationTime = it.action.time animationTime = it.action.time
it.dispose() it.dispose()
this.animation = null this.animation = null
} }
// Create a new mesh if necessary. // Create a new mesh if necessary.
if (ninjaGeometry != null) { if (ninjaGeometry != null) {
val obj3d = when (ninjaGeometry) { val obj3d = when (ninjaGeometry) {
is NinjaGeometry.Object -> { is NinjaGeometry.Object -> {
val obj = ninjaGeometry.obj val obj = ninjaGeometry.obj
if (obj is NjObject) { if (obj is NjObject) {
ninjaObjectToSkinnedMesh(obj, textures, boundingVolumes = true) ninjaObjectToSkinnedMesh(obj, textures, boundingVolumes = true)
} else { } else {
ninjaObjectToMesh(obj, textures, boundingVolumes = true) 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 context.scene.add(obj3d)
// Ninja geometry changes except when we're switching between character class models. this.obj3d = obj3d
val charClassActive = viewerStore.currentCharacterClass.value != null
val resetCamera = !charClassActive || !this.charClassActive
this.charClassActive = charClassActive
if (resetCamera) { if (obj3d.isSkinnedMesh() && ninjaGeometry is NinjaGeometry.Object) {
// Compute camera position. // Add skeleton.
val bSphere = boundingSphere(obj3d) val skeletonHelper = SkeletonHelper(obj3d)
val cameraDistFactor = skeletonHelper.visible = viewerStore.showSkeleton.value
1.5 / tan(degToRad((context.camera as PerspectiveCamera).fov) / 2) skeletonHelper.material.unsafeCast<LineBasicMaterial>().linewidth = 3
val cameraPos = CAMERA_POS * (bSphere.radius * cameraDistFactor)
inputManager.lookAt(cameraPos, bSphere.center)
}
context.scene.add(obj3d) context.scene.add(skeletonHelper)
this.obj3d = obj3d this.skeletonHelper = skeletonHelper
if (obj3d.isSkinnedMesh() && ninjaGeometry is NinjaGeometry.Object) { // Create a new animation mixer and clip.
// Add skeleton. viewerStore.currentNinjaMotion.value?.let { njMotion ->
val skeletonHelper = SkeletonHelper(obj3d) animation = Animation(ninjaGeometry.obj, njMotion, obj3d).also {
skeletonHelper.visible = viewerStore.showSkeleton.value it.mixer.timeScale = viewerStore.frameRate.value / PSO_FRAME_RATE_DOUBLE
skeletonHelper.material.unsafeCast<LineBasicMaterial>().linewidth = 3 it.action.time = animationTime ?: .0
it.action.play()
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()
} }
} }
} }

View File

@ -51,6 +51,7 @@ class ViewerStore(
private val _currentAnimation = mutableVal<AnimationModel?>(null) private val _currentAnimation = mutableVal<AnimationModel?>(null)
// Settings. // Settings.
private val _applyTextures = mutableVal(true)
private val _showSkeleton = mutableVal(false) private val _showSkeleton = mutableVal(false)
private val _animationPlaying = mutableVal(true) private val _animationPlaying = mutableVal(true)
private val _frameRate = mutableVal(PSO_FRAME_RATE) private val _frameRate = mutableVal(PSO_FRAME_RATE)
@ -74,6 +75,10 @@ class ViewerStore(
val currentAnimation: Val<AnimationModel?> = _currentAnimation val currentAnimation: Val<AnimationModel?> = _currentAnimation
// Settings. // Settings.
val applyTexturesEnabled: Val<Boolean> = _currentNinjaGeometry.map {
it == null || it !is NinjaGeometry.Collision
}
val applyTextures: Val<Boolean> = applyTexturesEnabled and _applyTextures
val showSkeletonEnabled: Val<Boolean> = _currentNinjaGeometry.map { val showSkeletonEnabled: Val<Boolean> = _currentNinjaGeometry.map {
it is NinjaGeometry.Object && it.obj is NjObject 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) { fun setShowSkeleton(show: Boolean) {
_showSkeleton.value = show _showSkeleton.value = show
} }

View File

@ -27,6 +27,12 @@ class ViewerToolbar(private val ctrl: ViewerToolbarController) : Widget() {
checked = ctrl.showSkeleton, checked = ctrl.showSkeleton,
onChange = ctrl::setShowSkeleton, onChange = ctrl::setShowSkeleton,
), ),
Checkbox(
label = "Apply textures",
enabled = ctrl.applyTexturesEnabled,
checked = ctrl.applyTextures,
onChange = ctrl::setApplyTextures,
),
Checkbox( Checkbox(
label = "Play animation", label = "Play animation",
enabled = ctrl.animationControlsEnabled, enabled = ctrl.animationControlsEnabled,