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
web/src/main/kotlin/world/phantasmal/web
core
externals/three
viewer

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

View File

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

View File

@ -28,6 +28,8 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
private val _resultDialogVisible = mutableVal(false)
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 showSkeleton: Val<Boolean> = store.showSkeleton
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) {
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.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<LineBasicMaterial>().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<LineBasicMaterial>().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()
}
}
}
}

View File

@ -51,6 +51,7 @@ class ViewerStore(
private val _currentAnimation = mutableVal<AnimationModel?>(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<AnimationModel?> = _currentAnimation
// 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 {
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
}

View File

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