mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
Converted from Babylon.js back to Three.js. Added texture viewer and entity textures.
This commit is contained in:
parent
d98b565766
commit
2fac7dbc39
@ -270,5 +270,5 @@ class BufferCursor(
|
||||
}
|
||||
}
|
||||
|
||||
fun Buffer.cursor(): BufferCursor =
|
||||
BufferCursor(this)
|
||||
fun Buffer.cursor(offset: Int = 0, size: Int = this.size - offset): BufferCursor =
|
||||
BufferCursor(this, offset, size)
|
||||
|
@ -16,17 +16,18 @@ class IffChunkHeader(val type: Int, val size: Int)
|
||||
* IFF files contain chunks preceded by an 8-byte header.
|
||||
* The header consists of 4 ASCII characters for the "Type ID" and a 32-bit integer specifying the chunk size.
|
||||
*/
|
||||
fun parseIff(cursor: Cursor): PwResult<List<IffChunk>> =
|
||||
parse(cursor) { chunkCursor, type, size -> IffChunk(type, chunkCursor.take(size)) }
|
||||
fun parseIff(cursor: Cursor, silent: Boolean = false): PwResult<List<IffChunk>> =
|
||||
parse(cursor, silent) { chunkCursor, type, size -> IffChunk(type, chunkCursor.take(size)) }
|
||||
|
||||
/**
|
||||
* Parses just the chunk headers.
|
||||
*/
|
||||
fun parseIffHeaders(cursor: Cursor): PwResult<List<IffChunkHeader>> =
|
||||
parse(cursor) { _, type, size -> IffChunkHeader(type, size) }
|
||||
fun parseIffHeaders(cursor: Cursor, silent: Boolean = false): PwResult<List<IffChunkHeader>> =
|
||||
parse(cursor, silent) { _, type, size -> IffChunkHeader(type, size) }
|
||||
|
||||
private fun <T> parse(
|
||||
cursor: Cursor,
|
||||
silent: Boolean,
|
||||
getChunk: (Cursor, type: Int, size: Int) -> T,
|
||||
): PwResult<List<T>> {
|
||||
val result = PwResult.build<List<T>>(logger)
|
||||
@ -40,11 +41,14 @@ private fun <T> parse(
|
||||
|
||||
if (size > cursor.bytesLeft) {
|
||||
corrupted = true
|
||||
result.addProblem(
|
||||
if (chunks.isEmpty()) Severity.Error else Severity.Warning,
|
||||
"IFF file corrupted.",
|
||||
"Size $size was too large (only ${cursor.bytesLeft} bytes left) at position $sizePos."
|
||||
)
|
||||
|
||||
if (!silent) {
|
||||
result.addProblem(
|
||||
if (chunks.isEmpty()) Severity.Error else Severity.Warning,
|
||||
"IFF file corrupted.",
|
||||
"Size $size was too large (only ${cursor.bytesLeft} bytes left) at position $sizePos."
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
@ -67,9 +67,9 @@ class NjTriangleStrip(
|
||||
val clockwiseWinding: Boolean,
|
||||
val hasTexCoords: Boolean,
|
||||
val hasNormal: Boolean,
|
||||
var textureId: UInt?,
|
||||
var srcAlpha: UByte?,
|
||||
var dstAlpha: UByte?,
|
||||
var textureId: Int?,
|
||||
var srcAlpha: Int?,
|
||||
var dstAlpha: Int?,
|
||||
val vertices: List<NjMeshVertex>,
|
||||
)
|
||||
|
||||
@ -84,11 +84,11 @@ sealed class NjChunk(val typeId: UByte) {
|
||||
|
||||
object Null : NjChunk(0u)
|
||||
|
||||
class Bits(typeId: UByte, val srcAlpha: UByte, val dstAlpha: UByte) : NjChunk(typeId)
|
||||
class Bits(typeId: UByte, val srcAlpha: Int, val dstAlpha: Int) : NjChunk(typeId)
|
||||
|
||||
class CachePolygonList(val cacheIndex: UByte, val offset: Int) : NjChunk(4u)
|
||||
class CachePolygonList(val cacheIndex: Int, val offset: Int) : NjChunk(4u)
|
||||
|
||||
class DrawPolygonList(val cacheIndex: UByte) : NjChunk(5u)
|
||||
class DrawPolygonList(val cacheIndex: Int) : NjChunk(5u)
|
||||
|
||||
class Tiny(
|
||||
typeId: UByte,
|
||||
@ -97,15 +97,15 @@ sealed class NjChunk(val typeId: UByte) {
|
||||
val clampU: Boolean,
|
||||
val clampV: Boolean,
|
||||
val mipmapDAdjust: UInt,
|
||||
val filterMode: UInt,
|
||||
val filterMode: Int,
|
||||
val superSample: Boolean,
|
||||
val textureId: UInt,
|
||||
val textureId: Int,
|
||||
) : NjChunk(typeId)
|
||||
|
||||
class Material(
|
||||
typeId: UByte,
|
||||
val srcAlpha: UByte,
|
||||
val dstAlpha: UByte,
|
||||
val srcAlpha: Int,
|
||||
val dstAlpha: Int,
|
||||
val diffuse: NjArgb?,
|
||||
val ambient: NjArgb?,
|
||||
val specular: NjErgb?,
|
||||
|
@ -13,11 +13,9 @@ import kotlin.math.abs
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private const val ZERO_U8: UByte = 0u
|
||||
|
||||
// TODO: Simplify parser by not parsing chunks into vertices and meshes. Do the chunk to vertex/mesh
|
||||
// conversion at a higher level.
|
||||
fun parseNjModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): NjModel {
|
||||
fun parseNjModel(cursor: Cursor, cachedChunkOffsets: MutableMap<Int, Int>): NjModel {
|
||||
val vlistOffset = cursor.int() // Vertex list
|
||||
val plistOffset = cursor.int() // Triangle strip index list
|
||||
val collisionSphereCenter = cursor.vec3Float()
|
||||
@ -50,9 +48,9 @@ fun parseNjModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): Nj
|
||||
if (plistOffset != 0) {
|
||||
cursor.seekStart(plistOffset)
|
||||
|
||||
var textureId: UInt? = null
|
||||
var srcAlpha: UByte? = null
|
||||
var dstAlpha: UByte? = null
|
||||
var textureId: Int? = null
|
||||
var srcAlpha: Int? = null
|
||||
var dstAlpha: Int? = null
|
||||
|
||||
for (chunk in parseChunks(cursor, cachedChunkOffsets, false)) {
|
||||
when (chunk) {
|
||||
@ -98,7 +96,7 @@ fun parseNjModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): Nj
|
||||
// TODO: don't reparse when DrawPolygonList chunk is encountered.
|
||||
private fun parseChunks(
|
||||
cursor: Cursor,
|
||||
cachedChunkOffsets: MutableMap<UByte, Int>,
|
||||
cachedChunkOffsets: MutableMap<Int, Int>,
|
||||
wideEndChunks: Boolean,
|
||||
): List<NjChunk> {
|
||||
val chunks: MutableList<NjChunk> = mutableListOf()
|
||||
@ -106,8 +104,7 @@ private fun parseChunks(
|
||||
|
||||
while (loop) {
|
||||
val typeId = cursor.uByte()
|
||||
val flags = cursor.uByte()
|
||||
val flagsUInt = flags.toUInt()
|
||||
val flags = cursor.uByte().toInt()
|
||||
val chunkStartPosition = cursor.position
|
||||
var size = 0
|
||||
|
||||
@ -118,8 +115,8 @@ private fun parseChunks(
|
||||
in 1..3 -> {
|
||||
chunks.add(NjChunk.Bits(
|
||||
typeId,
|
||||
srcAlpha = ((flagsUInt shr 3).toUByte() and 0b111u),
|
||||
dstAlpha = flags and 0b111u,
|
||||
srcAlpha = (flags ushr 3) and 0b111,
|
||||
dstAlpha = flags and 0b111,
|
||||
))
|
||||
}
|
||||
4 -> {
|
||||
@ -147,7 +144,7 @@ private fun parseChunks(
|
||||
}
|
||||
in 8..9 -> {
|
||||
size = 2
|
||||
val textureBitsAndId = cursor.uShort().toUInt()
|
||||
val textureBitsAndId = cursor.uShort().toInt()
|
||||
|
||||
chunks.add(NjChunk.Tiny(
|
||||
typeId,
|
||||
@ -156,9 +153,9 @@ private fun parseChunks(
|
||||
clampU = (typeId.toUInt() and 0x20u) != 0u,
|
||||
clampV = (typeId.toUInt() and 0x10u) != 0u,
|
||||
mipmapDAdjust = typeId.toUInt() and 0b1111u,
|
||||
filterMode = textureBitsAndId shr 14,
|
||||
superSample = (textureBitsAndId and 0x40u) != 0u,
|
||||
textureId = textureBitsAndId and 0x1fffu,
|
||||
filterMode = textureBitsAndId ushr 14,
|
||||
superSample = (textureBitsAndId and 0x40) != 0,
|
||||
textureId = textureBitsAndId and 0x1fff,
|
||||
))
|
||||
}
|
||||
in 17..31 -> {
|
||||
@ -168,7 +165,7 @@ private fun parseChunks(
|
||||
var ambient: NjArgb? = null
|
||||
var specular: NjErgb? = null
|
||||
|
||||
if ((flagsUInt and 0b1u) != 0u) {
|
||||
if ((flags and 0b1) != 0) {
|
||||
diffuse = NjArgb(
|
||||
b = cursor.uByte().toFloat() / 255f,
|
||||
g = cursor.uByte().toFloat() / 255f,
|
||||
@ -177,7 +174,7 @@ private fun parseChunks(
|
||||
)
|
||||
}
|
||||
|
||||
if ((flagsUInt and 0b10u) != 0u) {
|
||||
if ((flags and 0b10) != 0) {
|
||||
ambient = NjArgb(
|
||||
b = cursor.uByte().toFloat() / 255f,
|
||||
g = cursor.uByte().toFloat() / 255f,
|
||||
@ -186,7 +183,7 @@ private fun parseChunks(
|
||||
)
|
||||
}
|
||||
|
||||
if ((flagsUInt and 0b100u) != 0u) {
|
||||
if ((flags and 0b100) != 0) {
|
||||
specular = NjErgb(
|
||||
b = cursor.uByte(),
|
||||
g = cursor.uByte(),
|
||||
@ -197,8 +194,8 @@ private fun parseChunks(
|
||||
|
||||
chunks.add(NjChunk.Material(
|
||||
typeId,
|
||||
srcAlpha = ((flagsUInt shr 3).toUByte() and 0b111u),
|
||||
dstAlpha = flags and 0b111u,
|
||||
srcAlpha = (flags ushr 3) and 0b111,
|
||||
dstAlpha = flags and 0b111,
|
||||
diffuse,
|
||||
ambient,
|
||||
specular,
|
||||
@ -247,10 +244,10 @@ private fun parseChunks(
|
||||
private fun parseVertexChunk(
|
||||
cursor: Cursor,
|
||||
chunkTypeId: UByte,
|
||||
flags: UByte,
|
||||
flags: Int,
|
||||
): List<NjChunkVertex> {
|
||||
val boneWeightStatus = (flags and 0b11u).toInt()
|
||||
val calcContinue = (flags and 0x80u) != ZERO_U8
|
||||
val boneWeightStatus = flags and 0b11
|
||||
val calcContinue = (flags and 0x80) != 0
|
||||
|
||||
val index = cursor.uShort()
|
||||
val vertexCount = cursor.uShort()
|
||||
@ -333,15 +330,15 @@ private fun parseVertexChunk(
|
||||
private fun parseTriangleStripChunk(
|
||||
cursor: Cursor,
|
||||
chunkTypeId: UByte,
|
||||
flags: UByte,
|
||||
flags: Int,
|
||||
): List<NjTriangleStrip> {
|
||||
val ignoreLight = (flags and 0b1u) != ZERO_U8
|
||||
val ignoreSpecular = (flags and 0b10u) != ZERO_U8
|
||||
val ignoreAmbient = (flags and 0b100u) != ZERO_U8
|
||||
val useAlpha = (flags and 0b1000u) != ZERO_U8
|
||||
val doubleSide = (flags and 0b10000u) != ZERO_U8
|
||||
val flatShading = (flags and 0b100000u) != ZERO_U8
|
||||
val environmentMapping = (flags and 0b1000000u) != ZERO_U8
|
||||
val ignoreLight = (flags and 0b1) != 0
|
||||
val ignoreSpecular = (flags and 0b10) != 0
|
||||
val ignoreAmbient = (flags and 0b100) != 0
|
||||
val useAlpha = (flags and 0b1000) != 0
|
||||
val doubleSide = (flags and 0b10000) != 0
|
||||
val flatShading = (flags and 0b100000) != 0
|
||||
val environmentMapping = (flags and 0b1000000) != 0
|
||||
|
||||
val userOffsetAndStripCount = cursor.short().toInt()
|
||||
val userFlagsSize = (userOffsetAndStripCount ushr 14)
|
||||
|
@ -0,0 +1,103 @@
|
||||
package world.phantasmal.lib.fileFormats.ninja
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.Failure
|
||||
import world.phantasmal.core.PwResult
|
||||
import world.phantasmal.core.Severity
|
||||
import world.phantasmal.core.Success
|
||||
import world.phantasmal.lib.buffer.Buffer
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
import world.phantasmal.lib.fileFormats.parseIff
|
||||
import world.phantasmal.lib.fileFormats.parseIffHeaders
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private const val XVMH = 0x484d5658
|
||||
private const val XVRT = 0x54525658
|
||||
|
||||
class Xvm(
|
||||
val textures: List<XvrTexture>,
|
||||
)
|
||||
|
||||
class XvrTexture(
|
||||
val id: Int,
|
||||
val format: Pair<Int, Int>,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val size: Int,
|
||||
val data: Buffer,
|
||||
)
|
||||
|
||||
fun parseXvr(cursor: Cursor): XvrTexture {
|
||||
val format1 = cursor.int()
|
||||
val format2 = cursor.int()
|
||||
val id = cursor.int()
|
||||
val width = cursor.uShort().toInt()
|
||||
val height = cursor.uShort().toInt()
|
||||
val size = cursor.int()
|
||||
cursor.seek(36)
|
||||
val data = cursor.buffer(size)
|
||||
return XvrTexture(
|
||||
id,
|
||||
format = Pair(format1, format2),
|
||||
width,
|
||||
height,
|
||||
size,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
fun isXvm(cursor: Cursor): Boolean {
|
||||
val iffResult = parseIffHeaders(cursor, silent = true)
|
||||
cursor.seekStart(0)
|
||||
|
||||
return iffResult is Success &&
|
||||
iffResult.value.any { chunk -> chunk.type == XVMH || chunk.type == XVRT }
|
||||
}
|
||||
|
||||
fun parseXvm(cursor: Cursor): PwResult<Xvm> {
|
||||
val iffResult = parseIff(cursor)
|
||||
|
||||
if (iffResult !is Success) {
|
||||
return iffResult as Failure
|
||||
}
|
||||
|
||||
val result = PwResult.build<Xvm>(logger)
|
||||
result.addResult(iffResult)
|
||||
val chunks = iffResult.value
|
||||
val headerChunk = chunks.find { it.type == XVMH }
|
||||
val header = headerChunk?.data?.let(::parseHeader)
|
||||
|
||||
val textures = chunks
|
||||
.filter { it.type == XVRT }
|
||||
.map { parseXvr(it.data) }
|
||||
|
||||
if (header == null && textures.isEmpty()) {
|
||||
result.addProblem(
|
||||
Severity.Error,
|
||||
"Corrupted XVM file.",
|
||||
"No header and no XVRT chunks found.",
|
||||
)
|
||||
|
||||
return result.failure()
|
||||
}
|
||||
|
||||
if (header != null && header.textureCount != textures.size) {
|
||||
result.addProblem(
|
||||
Severity.Warning,
|
||||
"Corrupted XVM file.",
|
||||
"Found ${textures.size} textures instead of ${header.textureCount} as defined in the header.",
|
||||
)
|
||||
}
|
||||
|
||||
return result.success(Xvm(textures))
|
||||
}
|
||||
|
||||
private class Header(
|
||||
val textureCount: Int,
|
||||
)
|
||||
|
||||
private fun parseHeader(cursor: Cursor): Header {
|
||||
val textureCount = cursor.uShort().toInt()
|
||||
return Header(textureCount)
|
||||
}
|
@ -15,4 +15,11 @@ interface QuestEntity<Type : EntityType> {
|
||||
var position: Vec3
|
||||
|
||||
var rotation: Vec3
|
||||
|
||||
/**
|
||||
* Set the section-relative position.
|
||||
*/
|
||||
fun setPosition(x: Float, y: Float, z: Float)
|
||||
|
||||
fun setRotation(x: Float, y: Float, z: Float)
|
||||
}
|
||||
|
@ -78,9 +78,7 @@ class QuestNpc(
|
||||
override var position: Vec3
|
||||
get() = Vec3(data.getFloat(20), data.getFloat(24), data.getFloat(28))
|
||||
set(value) {
|
||||
data.setFloat(20, value.x)
|
||||
data.setFloat(24, value.y)
|
||||
data.setFloat(28, value.z)
|
||||
setPosition(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
override var rotation: Vec3
|
||||
@ -90,9 +88,7 @@ class QuestNpc(
|
||||
angleToRad(data.getInt(40)),
|
||||
)
|
||||
set(value) {
|
||||
data.setInt(32, radToAngle(value.x))
|
||||
data.setInt(36, radToAngle(value.y))
|
||||
data.setInt(40, radToAngle(value.z))
|
||||
setRotation(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,4 +117,16 @@ class QuestNpc(
|
||||
"Data size should be $NPC_BYTE_SIZE but was ${data.size}."
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPosition(x: Float, y: Float, z: Float) {
|
||||
data.setFloat(20, x)
|
||||
data.setFloat(24, y)
|
||||
data.setFloat(28, z)
|
||||
}
|
||||
|
||||
override fun setRotation(x: Float, y: Float, z: Float) {
|
||||
data.setInt(32, radToAngle(x))
|
||||
data.setInt(36, radToAngle(y))
|
||||
data.setInt(40, radToAngle(z))
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,7 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
|
||||
override var position: Vec3
|
||||
get() = Vec3(data.getFloat(16), data.getFloat(20), data.getFloat(24))
|
||||
set(value) {
|
||||
data.setFloat(16, value.x)
|
||||
data.setFloat(20, value.y)
|
||||
data.setFloat(24, value.z)
|
||||
setPosition(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
override var rotation: Vec3
|
||||
@ -40,9 +38,7 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
|
||||
angleToRad(data.getInt(36)),
|
||||
)
|
||||
set(value) {
|
||||
data.setInt(28, radToAngle(value.x))
|
||||
data.setInt(32, radToAngle(value.y))
|
||||
data.setInt(36, radToAngle(value.z))
|
||||
setRotation(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
val scriptLabel: Int?
|
||||
@ -96,4 +92,16 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
|
||||
"Data size should be $OBJECT_BYTE_SIZE but was ${data.size}."
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPosition(x: Float, y: Float, z: Float) {
|
||||
data.setFloat(16, x)
|
||||
data.setFloat(20, y)
|
||||
data.setFloat(24, z)
|
||||
}
|
||||
|
||||
override fun setRotation(x: Float, y: Float, z: Float) {
|
||||
data.setInt(28, radToAngle(x))
|
||||
data.setInt(32, radToAngle(y))
|
||||
data.setInt(36, radToAngle(z))
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,13 @@ import org.khronos.webgl.Uint8Array
|
||||
import world.phantasmal.lib.Endianness
|
||||
|
||||
actual class Buffer private constructor(
|
||||
private var arrayBuffer: ArrayBuffer,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
size: Int,
|
||||
endianness: Endianness,
|
||||
) {
|
||||
var arrayBuffer = arrayBuffer
|
||||
private set
|
||||
|
||||
private var dataView = DataView(arrayBuffer)
|
||||
private var littleEndian = endianness == Endianness.Little
|
||||
|
||||
|
@ -40,9 +40,9 @@ dependencies {
|
||||
implementation("io.ktor:ktor-client-core-js:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-serialization-js:$ktorVersion")
|
||||
implementation("org.jetbrains.kotlin:kotlin-serialization:$serializationVersion")
|
||||
implementation(npm("@babylonjs/core", "^4.2.0"))
|
||||
implementation(npm("golden-layout", "^1.5.9"))
|
||||
implementation(npm("monaco-editor", "^0.21.2"))
|
||||
implementation(npm("three", "^0.122.0"))
|
||||
|
||||
implementation(devNpm("file-loader", "^6.0.0"))
|
||||
implementation(devNpm("monaco-editor-webpack-plugin", "^2.0.0"))
|
||||
|
@ -18,10 +18,14 @@ import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.application.Application
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.logging.LogAppender
|
||||
import world.phantasmal.web.core.logging.LogFormatter
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.stores.ApplicationUrl
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.three.WebGLRenderer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
import world.phantasmal.webui.dom.root
|
||||
import world.phantasmal.webui.obj
|
||||
|
||||
fun main() {
|
||||
if (document.body != null) {
|
||||
@ -33,6 +37,7 @@ fun main() {
|
||||
|
||||
private fun init(): Disposable {
|
||||
KotlinLoggingConfiguration.FORMATTER = LogFormatter()
|
||||
KotlinLoggingConfiguration.APPENDER = LogAppender()
|
||||
|
||||
if (window.location.hostname == "localhost") {
|
||||
KotlinLoggingConfiguration.LOG_LEVEL = KotlinLoggingLevel.TRACE
|
||||
@ -60,14 +65,31 @@ private fun init(): Disposable {
|
||||
rootElement,
|
||||
AssetLoader(httpClient),
|
||||
disposer.add(HistoryApplicationUrl()),
|
||||
createEngine = { Engine(it) }
|
||||
::createThreeRenderer,
|
||||
)
|
||||
)
|
||||
|
||||
return disposer
|
||||
}
|
||||
|
||||
class HistoryApplicationUrl : TrackedDisposable(), ApplicationUrl {
|
||||
private fun createThreeRenderer(): DisposableThreeRenderer =
|
||||
object : TrackedDisposable(), DisposableThreeRenderer {
|
||||
override val renderer = WebGLRenderer(obj {
|
||||
antialias = true
|
||||
alpha = true
|
||||
})
|
||||
|
||||
init {
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
renderer.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
}
|
||||
|
||||
private class HistoryApplicationUrl : TrackedDisposable(), ApplicationUrl {
|
||||
private val path: String get() = window.location.pathname
|
||||
|
||||
override val url = mutableVal(window.location.hash.substring(1))
|
||||
|
@ -3,7 +3,6 @@ package world.phantasmal.web.application
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.DragEvent
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
@ -14,9 +13,9 @@ import world.phantasmal.web.application.widgets.MainContentWidget
|
||||
import world.phantasmal.web.application.widgets.NavigationWidget
|
||||
import world.phantasmal.web.core.PwTool
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.stores.ApplicationUrl
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.huntOptimizer.HuntOptimizer
|
||||
import world.phantasmal.web.questEditor.QuestEditor
|
||||
import world.phantasmal.web.viewer.Viewer
|
||||
@ -28,7 +27,7 @@ class Application(
|
||||
rootElement: HTMLElement,
|
||||
assetLoader: AssetLoader,
|
||||
applicationUrl: ApplicationUrl,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : DisposableContainer() {
|
||||
init {
|
||||
addDisposables(
|
||||
@ -49,8 +48,8 @@ class Application(
|
||||
|
||||
// The various tools Phantasmal World consists of.
|
||||
val tools: List<PwTool> = listOf(
|
||||
Viewer(createEngine),
|
||||
QuestEditor(assetLoader, uiStore, createEngine),
|
||||
Viewer(createThreeRenderer),
|
||||
QuestEditor(assetLoader, uiStore, createThreeRenderer),
|
||||
HuntOptimizer(assetLoader, uiStore),
|
||||
)
|
||||
|
||||
|
@ -1,74 +0,0 @@
|
||||
package world.phantasmal.web.core
|
||||
|
||||
import world.phantasmal.web.externals.babylon.Matrix
|
||||
import world.phantasmal.web.externals.babylon.Quaternion
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
|
||||
operator fun Vector3.plus(other: Vector3): Vector3 =
|
||||
add(other)
|
||||
|
||||
operator fun Vector3.plusAssign(other: Vector3) {
|
||||
addInPlace(other)
|
||||
}
|
||||
|
||||
operator fun Vector3.minus(other: Vector3): Vector3 =
|
||||
subtract(other)
|
||||
|
||||
operator fun Vector3.minusAssign(other: Vector3) {
|
||||
subtractInPlace(other)
|
||||
}
|
||||
|
||||
operator fun Vector3.times(scalar: Double): Vector3 =
|
||||
scale(scalar)
|
||||
|
||||
infix fun Vector3.dot(other: Vector3): Double =
|
||||
Vector3.Dot(this, other)
|
||||
|
||||
infix fun Vector3.cross(other: Vector3): Vector3 =
|
||||
cross(other)
|
||||
|
||||
operator fun Matrix.timesAssign(other: Matrix) {
|
||||
other.preMultiply(this)
|
||||
}
|
||||
|
||||
fun Matrix.preMultiply(other: Matrix) {
|
||||
// Multiplies this by other.
|
||||
multiplyToRef(other, this)
|
||||
}
|
||||
|
||||
fun Matrix.multiply(v: Vector3) {
|
||||
Vector3.TransformCoordinatesToRef(v, this, v)
|
||||
}
|
||||
|
||||
fun Matrix.multiply3x3(v: Vector3) {
|
||||
Vector3.TransformNormalToRef(v, this, v)
|
||||
}
|
||||
|
||||
operator fun Quaternion.timesAssign(other: Quaternion) {
|
||||
multiplyInPlace(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new quaternion that's the inverse of this quaternion.
|
||||
*/
|
||||
fun Quaternion.inverse(): Quaternion = Quaternion.Inverse(this)
|
||||
|
||||
/**
|
||||
* Inverts this quaternion.
|
||||
*/
|
||||
fun Quaternion.invert() {
|
||||
Quaternion.InverseToRef(this, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [p] by this versor.
|
||||
*/
|
||||
fun Quaternion.transform(p: Vector3) {
|
||||
p.rotateByQuaternionToRef(this, p)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new point equal to [p] transformed by this versor.
|
||||
*/
|
||||
fun Quaternion.transformed(p: Vector3): Vector3 =
|
||||
p.rotateByQuaternionToRef(this, Vector3.Zero())
|
@ -0,0 +1,56 @@
|
||||
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
|
||||
|
||||
operator fun Vector3.plus(other: Vector3): Vector3 =
|
||||
clone().add(other)
|
||||
|
||||
operator fun Vector3.plusAssign(other: Vector3) {
|
||||
add(other)
|
||||
}
|
||||
|
||||
operator fun Vector3.minus(other: Vector3): Vector3 =
|
||||
clone().sub(other)
|
||||
|
||||
operator fun Vector3.minusAssign(other: Vector3) {
|
||||
sub(other)
|
||||
}
|
||||
|
||||
operator fun Vector3.times(scalar: Double): Vector3 =
|
||||
clone().multiplyScalar(scalar)
|
||||
|
||||
infix fun Vector3.dot(other: Vector3): Double =
|
||||
dot(other)
|
||||
|
||||
infix fun Vector3.cross(other: Vector3): Vector3 =
|
||||
cross(other)
|
||||
|
||||
operator fun Quaternion.timesAssign(other: Quaternion) {
|
||||
multiply(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [Euler] object from a [Quaternion] with the correct rotation order.
|
||||
*/
|
||||
fun Quaternion.toEuler(): Euler =
|
||||
Euler().setFromQuaternion(this, "ZXY")
|
||||
|
||||
/**
|
||||
* Creates an [Euler] object with the correct rotation order.
|
||||
*/
|
||||
fun euler(x: Float, y: Float, z: Float): Euler =
|
||||
euler(x.toDouble(), y.toDouble(), z.toDouble())
|
||||
|
||||
/**
|
||||
* Creates an [Euler] object with the correct rotation order.
|
||||
*/
|
||||
fun euler(x: Double, y: Double, z: Double): Euler =
|
||||
Euler(x, y, z, "ZXY")
|
||||
|
||||
/**
|
||||
* Creates an [Euler] object from a [Quaternion] with the correct rotation order.
|
||||
*/
|
||||
fun Euler.toQuaternion(): Quaternion =
|
||||
Quaternion().setFromEuler(this)
|
@ -0,0 +1,45 @@
|
||||
package world.phantasmal.web.core.logging
|
||||
|
||||
import mu.Appender
|
||||
|
||||
class LogAppender : Appender {
|
||||
override fun trace(message: Any?) {
|
||||
if (message is MessageWithThrowable) {
|
||||
console.log(message.message, message.throwable)
|
||||
} else {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun debug(message: Any?) {
|
||||
if (message is MessageWithThrowable) {
|
||||
console.log(message.message, message.throwable)
|
||||
} else {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun info(message: Any?) {
|
||||
if (message is MessageWithThrowable) {
|
||||
console.info(message.message, message.throwable)
|
||||
} else {
|
||||
console.info(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun warn(message: Any?) {
|
||||
if (message is MessageWithThrowable) {
|
||||
console.warn(message.message, message.throwable)
|
||||
} else {
|
||||
console.warn(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(message: Any?) {
|
||||
if (message is MessageWithThrowable) {
|
||||
console.error(message.message, message.throwable)
|
||||
} else {
|
||||
console.error(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.web
|
||||
package world.phantasmal.web.core.logging
|
||||
|
||||
import mu.DefaultMessageFormatter
|
||||
import mu.Formatter
|
||||
import mu.KotlinLoggingLevel
|
||||
import mu.Marker
|
||||
@ -12,15 +11,15 @@ class LogFormatter : Formatter {
|
||||
loggerName: String,
|
||||
msg: () -> Any?,
|
||||
): String =
|
||||
time() + DefaultMessageFormatter.formatMessage(level, loggerName, msg)
|
||||
"${time()} ${level.str()} $loggerName - ${msg.toStringSafe()}"
|
||||
|
||||
override fun formatMessage(
|
||||
level: KotlinLoggingLevel,
|
||||
loggerName: String,
|
||||
t: Throwable?,
|
||||
msg: () -> Any?,
|
||||
): String =
|
||||
time() + DefaultMessageFormatter.formatMessage(level, loggerName, t, msg)
|
||||
): MessageWithThrowable =
|
||||
MessageWithThrowable(formatMessage(level, loggerName, msg), t)
|
||||
|
||||
override fun formatMessage(
|
||||
level: KotlinLoggingLevel,
|
||||
@ -28,7 +27,7 @@ class LogFormatter : Formatter {
|
||||
marker: Marker?,
|
||||
msg: () -> Any?,
|
||||
): String =
|
||||
time() + DefaultMessageFormatter.formatMessage(level, loggerName, marker, msg)
|
||||
"${time()} ${level.str()} $loggerName [${marker?.getName()}] - ${msg.toStringSafe()}"
|
||||
|
||||
override fun formatMessage(
|
||||
level: KotlinLoggingLevel,
|
||||
@ -36,8 +35,20 @@ class LogFormatter : Formatter {
|
||||
marker: Marker?,
|
||||
t: Throwable?,
|
||||
msg: () -> Any?,
|
||||
): String =
|
||||
time() + DefaultMessageFormatter.formatMessage(level, loggerName, marker, t, msg)
|
||||
): MessageWithThrowable =
|
||||
MessageWithThrowable(formatMessage(level, loggerName, marker, msg), t)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun (() -> Any?).toStringSafe(): String {
|
||||
return try {
|
||||
invoke().toString()
|
||||
} catch (e: Exception) {
|
||||
"Log message invocation failed: $e"
|
||||
}
|
||||
}
|
||||
|
||||
private fun KotlinLoggingLevel.str(): String =
|
||||
name.padEnd(MIN_LEVEL_LEN)
|
||||
|
||||
private fun time(): String {
|
||||
val date = Date()
|
||||
@ -47,4 +58,9 @@ class LogFormatter : Formatter {
|
||||
val ms = date.getMilliseconds().toString().padStart(3, '0')
|
||||
return "$h:$m:$s.$ms "
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MIN_LEVEL_LEN: Int =
|
||||
KotlinLoggingLevel.values().map { it.name.length }.maxOrNull()!!
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package world.phantasmal.web.core.logging
|
||||
|
||||
class MessageWithThrowable(
|
||||
val message: Any?,
|
||||
val throwable: Throwable?,
|
||||
)
|
@ -0,0 +1,27 @@
|
||||
package world.phantasmal.web.core.rendering
|
||||
|
||||
import world.phantasmal.web.externals.three.Object3D
|
||||
|
||||
/**
|
||||
* Recursively disposes any geometries/materials/textures attached to the given [Object3D] or its
|
||||
* children.
|
||||
*/
|
||||
fun disposeObject3DResources(obj: Object3D) {
|
||||
val dynObj = obj.asDynamic()
|
||||
|
||||
dynObj.geometry?.dispose()
|
||||
|
||||
if (dynObj.material is Array<*>) {
|
||||
for (material in dynObj.material) {
|
||||
material.map?.dispose()
|
||||
material.dispose()
|
||||
}
|
||||
} else if (dynObj.material != null) {
|
||||
dynObj.material.map?.dispose()
|
||||
dynObj.material.dispose()
|
||||
}
|
||||
|
||||
for (child in obj.children) {
|
||||
disposeObject3DResources(child)
|
||||
}
|
||||
}
|
@ -1,53 +1,115 @@
|
||||
package world.phantasmal.web.core.rendering
|
||||
|
||||
import kotlinx.browser.window
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.obj
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import world.phantasmal.web.externals.three.Renderer as ThreeRenderer
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
interface DisposableThreeRenderer : Disposable {
|
||||
val renderer: ThreeRenderer
|
||||
}
|
||||
|
||||
abstract class Renderer(
|
||||
val canvas: HTMLCanvasElement,
|
||||
val engine: Engine,
|
||||
createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
val camera: Camera,
|
||||
) : DisposableContainer() {
|
||||
private val light: HemisphericLight
|
||||
private val threeRenderer: ThreeRenderer = addDisposable(createThreeRenderer()).renderer
|
||||
private val light = HemisphereLight(
|
||||
skyColor = 0xffffff,
|
||||
groundColor = 0x505050,
|
||||
intensity = 1.0
|
||||
)
|
||||
private val lightHolder = Group().add(light)
|
||||
|
||||
abstract val camera: Camera
|
||||
private var rendering = false
|
||||
private var animationFrameHandle: Int = 0
|
||||
|
||||
val scene = Scene(engine)
|
||||
|
||||
init {
|
||||
with(scene) {
|
||||
useRightHandedSystem = true
|
||||
clearColor = Color4.FromInts(0x18, 0x18, 0x18, 0xFF)
|
||||
val canvas: HTMLCanvasElement =
|
||||
threeRenderer.domElement.apply {
|
||||
tabIndex = 0
|
||||
style.outline = "none"
|
||||
}
|
||||
|
||||
light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene)
|
||||
}
|
||||
val scene: Scene =
|
||||
Scene().apply {
|
||||
background = Color(0x181818)
|
||||
add(lightHolder)
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
camera.dispose()
|
||||
light.dispose()
|
||||
scene.dispose()
|
||||
engine.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
val controls: OrbitControls =
|
||||
OrbitControls(camera, canvas).apply {
|
||||
mouseButtons = obj {
|
||||
LEFT = MOUSE.PAN
|
||||
MIDDLE = MOUSE.DOLLY
|
||||
RIGHT = MOUSE.ROTATE
|
||||
}
|
||||
}
|
||||
|
||||
fun startRendering() {
|
||||
logger.trace { "${this::class.simpleName} - start rendering." }
|
||||
engine.runRenderLoop(::render)
|
||||
|
||||
if (!rendering) {
|
||||
rendering = true
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRendering() {
|
||||
logger.trace { "${this::class.simpleName} - stop rendering." }
|
||||
engine.stopRenderLoop()
|
||||
|
||||
rendering = false
|
||||
window.cancelAnimationFrame(animationFrameHandle)
|
||||
}
|
||||
|
||||
open fun setSize(width: Double, height: Double) {
|
||||
canvas.width = floor(width).toInt()
|
||||
canvas.height = floor(height).toInt()
|
||||
threeRenderer.setSize(width, height)
|
||||
|
||||
if (camera is PerspectiveCamera) {
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
} else if (camera is OrthographicCamera) {
|
||||
camera.left = -floor(width / 2)
|
||||
camera.right = ceil(width / 2)
|
||||
camera.top = floor(height / 2)
|
||||
camera.bottom = -ceil(height / 2)
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
controls.update()
|
||||
}
|
||||
|
||||
protected open fun render() {
|
||||
val lightDirection = Vector3(-1.0, 1.0, 1.0)
|
||||
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
|
||||
light.direction = lightDirection
|
||||
scene.render()
|
||||
if (camera is PerspectiveCamera) {
|
||||
val distance = (controls.target - camera.position).length()
|
||||
camera.near = distance / 100
|
||||
camera.far = max(2_000.0, 10 * distance)
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
threeRenderer.render(scene, camera)
|
||||
}
|
||||
|
||||
private fun renderLoop() {
|
||||
if (rendering) {
|
||||
animationFrameHandle = window.requestAnimationFrame {
|
||||
try {
|
||||
render()
|
||||
} finally {
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ package world.phantasmal.web.core.rendering.conversion
|
||||
|
||||
import world.phantasmal.lib.fileFormats.Vec2
|
||||
import world.phantasmal.lib.fileFormats.Vec3
|
||||
import world.phantasmal.web.externals.babylon.Vector2
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.externals.three.Vector2
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
|
||||
fun vec2ToBabylon(v: Vec2): Vector2 = Vector2(v.x.toDouble(), v.y.toDouble())
|
||||
fun vec2ToThree(v: Vec2): Vector2 = Vector2(v.x.toDouble(), v.y.toDouble())
|
||||
|
||||
fun vec3ToBabylon(v: Vec3): Vector3 = Vector3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble())
|
||||
fun vec3ToThree(v: Vec3): Vector3 = Vector3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble())
|
||||
|
||||
fun babylonToVec3(v: Vector3): Vec3 = Vec3(v.x.toFloat(), v.y.toFloat(), v.z.toFloat())
|
||||
fun threeToVec3(v: Vector3): Vec3 = Vec3(v.x.toFloat(), v.y.toFloat(), v.z.toFloat())
|
||||
|
@ -0,0 +1,186 @@
|
||||
package world.phantasmal.web.core.rendering.conversion
|
||||
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Uint16Array
|
||||
import org.khronos.webgl.set
|
||||
import world.phantasmal.lib.fileFormats.ninja.XvrTexture
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.web.viewer.rendering.xvrTextureToThree
|
||||
import world.phantasmal.webui.obj
|
||||
|
||||
class MeshBuilder {
|
||||
private val positions = mutableListOf<Vector3>()
|
||||
private val normals = mutableListOf<Vector3>()
|
||||
private val uvs = mutableListOf<Vector2>()
|
||||
|
||||
/**
|
||||
* One group per material.
|
||||
*/
|
||||
private val groups = mutableListOf<Group>()
|
||||
private val textures = mutableListOf<XvrTexture>()
|
||||
|
||||
fun getGroupIndex(
|
||||
textureId: Int?,
|
||||
alpha: Boolean,
|
||||
additiveBlending: Boolean,
|
||||
): Int {
|
||||
val idx = groups.indexOfFirst {
|
||||
it.textureId == textureId &&
|
||||
it.alpha == alpha &&
|
||||
it.additiveBlending == additiveBlending
|
||||
}
|
||||
|
||||
return if (idx != -1) {
|
||||
idx
|
||||
} else {
|
||||
groups.add(Group(textureId, alpha, additiveBlending))
|
||||
groups.lastIndex
|
||||
}
|
||||
}
|
||||
|
||||
val vertexCount: Int
|
||||
get() = positions.size
|
||||
|
||||
fun getPosition(index: Int): Vector3 =
|
||||
positions[index]
|
||||
|
||||
fun getNormal(index: Int): Vector3 =
|
||||
normals[index]
|
||||
|
||||
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2? = null) {
|
||||
positions.add(position)
|
||||
normals.add(normal)
|
||||
uv?.let { uvs.add(uv) }
|
||||
}
|
||||
|
||||
fun addIndex(groupIdx: Int, index: Int) {
|
||||
groups[groupIdx].indices.add(index.toShort())
|
||||
}
|
||||
|
||||
fun addBoneWeight(groupIdx: Int, index: Int, weight: Float) {
|
||||
val group = groups[groupIdx]
|
||||
group.boneIndices.add(index.toShort())
|
||||
group.boneWeights.add(weight)
|
||||
}
|
||||
|
||||
fun addTextures(textures: List<XvrTexture>) {
|
||||
this.textures.addAll(textures)
|
||||
}
|
||||
|
||||
fun buildMesh(boundingVolumes: Boolean = false): Mesh =
|
||||
build().let { (geom, materials) ->
|
||||
if (boundingVolumes) {
|
||||
geom.computeBoundingBox()
|
||||
geom.computeBoundingSphere()
|
||||
}
|
||||
|
||||
Mesh(geom, materials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [InstancedMesh] with 0 instances.
|
||||
*/
|
||||
fun buildInstancedMesh(maxInstances: Int, boundingVolumes: Boolean = false): InstancedMesh =
|
||||
build().let { (geom, materials) ->
|
||||
if (boundingVolumes) {
|
||||
geom.computeBoundingBox()
|
||||
geom.computeBoundingSphere()
|
||||
}
|
||||
|
||||
InstancedMesh(geom, materials, maxInstances).apply {
|
||||
// Start with 0 instances.
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun build(): Pair<BufferGeometry, Array<Material>> {
|
||||
check(this.positions.size == this.normals.size)
|
||||
check(this.uvs.isEmpty() || this.positions.size == this.uvs.size)
|
||||
|
||||
val positions = Float32Array(3 * positions.size)
|
||||
val normals = Float32Array(3 * normals.size)
|
||||
val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size)
|
||||
|
||||
for (i in this.positions.indices) {
|
||||
val pos = this.positions[i]
|
||||
positions[3 * i] = pos.x.toFloat()
|
||||
positions[3 * i + 1] = pos.y.toFloat()
|
||||
positions[3 * i + 2] = pos.z.toFloat()
|
||||
|
||||
val normal = this.normals[i]
|
||||
normals[3 * i] = normal.x.toFloat()
|
||||
normals[3 * i + 1] = normal.y.toFloat()
|
||||
normals[3 * i + 2] = normal.z.toFloat()
|
||||
|
||||
uvs?.let {
|
||||
val uv = this.uvs[i]
|
||||
uvs[2 * i] = uv.x.toFloat()
|
||||
uvs[2 * i + 1] = uv.y.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
val geom = BufferGeometry()
|
||||
geom.setAttribute("position", Float32BufferAttribute(positions, 3))
|
||||
geom.setAttribute("normal", Float32BufferAttribute(normals, 3))
|
||||
uvs?.let { geom.setAttribute("uv", Float32BufferAttribute(uvs, 2)) }
|
||||
val indices = Uint16Array(groups.sumBy { it.indices.size })
|
||||
|
||||
var offset = 0
|
||||
val texCache = mutableMapOf<Int, Texture?>()
|
||||
|
||||
val materials = mutableListOf<Material>()
|
||||
|
||||
for (group in groups) {
|
||||
indices.set(group.indices.toTypedArray(), offset)
|
||||
geom.addGroup(offset, group.indices.size, materials.size)
|
||||
|
||||
val tex = group.textureId?.let { texId ->
|
||||
texCache.getOrPut(texId) {
|
||||
textures.getOrNull(texId)?.let { xvm ->
|
||||
xvrTextureToThree(xvm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mat = if (tex == null) {
|
||||
MeshLambertMaterial(obj {
|
||||
// TODO: skinning
|
||||
side = DoubleSide
|
||||
})
|
||||
} else {
|
||||
MeshBasicMaterial(obj {
|
||||
map = tex
|
||||
side = DoubleSide
|
||||
|
||||
if (group.alpha) {
|
||||
transparent = true
|
||||
alphaTest = 0.01
|
||||
}
|
||||
|
||||
if (group.additiveBlending) {
|
||||
transparent = true
|
||||
alphaTest = 0.01
|
||||
blending = AdditiveBlending
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
materials.add(mat)
|
||||
offset += group.indices.size
|
||||
}
|
||||
|
||||
geom.setIndex(Uint16BufferAttribute(indices, 1))
|
||||
|
||||
return Pair(geom, materials.toTypedArray())
|
||||
}
|
||||
|
||||
private class Group(
|
||||
val textureId: Int?,
|
||||
val alpha: Boolean,
|
||||
val additiveBlending: Boolean,
|
||||
) {
|
||||
val indices = mutableListOf<Short>()
|
||||
val boneIndices = mutableListOf<Short>()
|
||||
val boneWeights = mutableListOf<Float>()
|
||||
}
|
||||
}
|
@ -1,56 +1,80 @@
|
||||
package world.phantasmal.web.core.rendering.conversion
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.lib.fileFormats.Vec3
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.lib.fileFormats.ninja.NjModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.XjModel
|
||||
import world.phantasmal.web.core.*
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import world.phantasmal.lib.fileFormats.ninja.*
|
||||
import world.phantasmal.web.core.cross
|
||||
import world.phantasmal.web.core.dot
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.core.toQuaternion
|
||||
import world.phantasmal.web.externals.three.*
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val DEFAULT_NORMAL = Vector3.Up()
|
||||
private val DEFAULT_UV = Vector2.Zero()
|
||||
private val NO_TRANSLATION = Vector3.Zero()
|
||||
private val NO_ROTATION = Quaternion.Identity()
|
||||
private val NO_SCALE = Vector3.One()
|
||||
private val DEFAULT_NORMAL = Vector3(0.0, 1.0, 0.0)
|
||||
private val DEFAULT_UV = Vector2(0.0, 0.0)
|
||||
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)
|
||||
|
||||
fun ninjaObjectToVertexData(ninjaObject: NinjaObject<*>): VertexData =
|
||||
NinjaToVertexDataConverter(VertexDataBuilder()).convert(ninjaObject)
|
||||
|
||||
fun ninjaObjectToVertexDataBuilder(
|
||||
fun ninjaObjectToMesh(
|
||||
ninjaObject: NinjaObject<*>,
|
||||
builder: VertexDataBuilder,
|
||||
): VertexData =
|
||||
NinjaToVertexDataConverter(builder).convert(ninjaObject)
|
||||
textures: List<XvrTexture>,
|
||||
boundingVolumes: Boolean = false
|
||||
): Mesh {
|
||||
val builder = MeshBuilder()
|
||||
builder.addTextures(textures)
|
||||
NinjaToMeshConverter(builder).convert(ninjaObject)
|
||||
return builder.buildMesh(boundingVolumes)
|
||||
}
|
||||
|
||||
fun ninjaObjectToInstancedMesh(
|
||||
ninjaObject: NinjaObject<*>,
|
||||
textures: List<XvrTexture>,
|
||||
maxInstances: Int,
|
||||
boundingVolumes: Boolean = false,
|
||||
): InstancedMesh {
|
||||
val builder = MeshBuilder()
|
||||
builder.addTextures(textures)
|
||||
NinjaToMeshConverter(builder).convert(ninjaObject)
|
||||
return builder.buildInstancedMesh(maxInstances, boundingVolumes)
|
||||
}
|
||||
|
||||
fun ninjaObjectToMeshBuilder(
|
||||
ninjaObject: NinjaObject<*>,
|
||||
builder: MeshBuilder,
|
||||
) {
|
||||
NinjaToMeshConverter(builder).convert(ninjaObject)
|
||||
}
|
||||
|
||||
// TODO: take into account different kinds of meshes/vertices (with or without normals, uv, etc.).
|
||||
private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) {
|
||||
private class NinjaToMeshConverter(private val builder: MeshBuilder) {
|
||||
private val vertexHolder = VertexHolder()
|
||||
private var boneIndex = 0
|
||||
|
||||
fun convert(ninjaObject: NinjaObject<*>): VertexData {
|
||||
objectToVertexData(ninjaObject, Matrix.Identity())
|
||||
return builder.build()
|
||||
fun convert(ninjaObject: NinjaObject<*>) {
|
||||
convertObject(ninjaObject, Matrix4())
|
||||
}
|
||||
|
||||
private fun objectToVertexData(obj: NinjaObject<*>, parentMatrix: Matrix) {
|
||||
private fun convertObject(obj: NinjaObject<*>, parentMatrix: Matrix4) {
|
||||
val ef = obj.evaluationFlags
|
||||
|
||||
val matrix = Matrix.Compose(
|
||||
if (ef.noScale) NO_SCALE else vec3ToBabylon(obj.scale),
|
||||
if (ef.noRotate) NO_ROTATION else eulerToQuat(obj.rotation, ef.zxyRotationOrder),
|
||||
if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position),
|
||||
val euler = Euler(
|
||||
obj.rotation.x.toDouble(),
|
||||
obj.rotation.y.toDouble(),
|
||||
obj.rotation.z.toDouble(),
|
||||
if (ef.zxyRotationOrder) "ZXY" else "ZYX",
|
||||
)
|
||||
matrix.preMultiply(parentMatrix)
|
||||
val matrix = Matrix4()
|
||||
.compose(
|
||||
if (ef.noTranslate) NO_TRANSLATION else vec3ToThree(obj.position),
|
||||
if (ef.noRotate) NO_ROTATION else euler.toQuaternion(),
|
||||
if (ef.noScale) NO_SCALE else vec3ToThree(obj.scale),
|
||||
)
|
||||
.premultiply(parentMatrix)
|
||||
|
||||
if (!ef.hidden) {
|
||||
obj.model?.let { model ->
|
||||
modelToVertexData(model, matrix)
|
||||
convertModel(model, matrix)
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,28 +82,27 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
|
||||
if (!ef.breakChildTrace) {
|
||||
obj.children.forEach { child ->
|
||||
objectToVertexData(child, matrix)
|
||||
convertObject(child, matrix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun modelToVertexData(model: NinjaModel, matrix: Matrix) =
|
||||
private fun convertModel(model: NinjaModel, matrix: Matrix4) =
|
||||
when (model) {
|
||||
is NjModel -> njModelToVertexData(model, matrix)
|
||||
is XjModel -> xjModelToVertexData(model, matrix)
|
||||
is NjModel -> convertNjModel(model, matrix)
|
||||
is XjModel -> convertXjModel(model, matrix)
|
||||
}
|
||||
|
||||
private fun njModelToVertexData(model: NjModel, matrix: Matrix) {
|
||||
val normalMatrix = Matrix.Identity()
|
||||
matrix.toNormalMatrix(normalMatrix)
|
||||
private fun convertNjModel(model: NjModel, matrix: Matrix4) {
|
||||
val normalMatrix = Matrix3().getNormalMatrix(matrix)
|
||||
|
||||
val newVertices = model.vertices.map { vertex ->
|
||||
vertex?.let {
|
||||
val position = vec3ToBabylon(vertex.position)
|
||||
val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
||||
val position = vec3ToThree(vertex.position)
|
||||
val normal = vertex.normal?.let(::vec3ToThree) ?: Vector3(0.0, 1.0, 0.0)
|
||||
|
||||
matrix.multiply(position)
|
||||
normalMatrix.multiply3x3(normal)
|
||||
position.applyMatrix4(matrix)
|
||||
normal.applyMatrix3(normalMatrix)
|
||||
|
||||
Vertex(
|
||||
boneIndex,
|
||||
@ -95,7 +118,11 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
vertexHolder.add(newVertices)
|
||||
|
||||
for (mesh in model.meshes) {
|
||||
val startIndexCount = builder.indexCount
|
||||
val group = builder.getGroupIndex(
|
||||
mesh.textureId,
|
||||
alpha = mesh.useAlpha,
|
||||
additiveBlending = mesh.srcAlpha != 4 || mesh.dstAlpha != 5
|
||||
)
|
||||
var i = 0
|
||||
|
||||
for (meshVertex in mesh.vertices) {
|
||||
@ -108,24 +135,24 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
} else {
|
||||
val vertex = vertices.last()
|
||||
val normal =
|
||||
vertex.normal ?: meshVertex.normal?.let(::vec3ToBabylon) ?: DEFAULT_NORMAL
|
||||
vertex.normal ?: meshVertex.normal?.let(::vec3ToThree) ?: DEFAULT_NORMAL
|
||||
val index = builder.vertexCount
|
||||
|
||||
builder.addVertex(
|
||||
vertex.position,
|
||||
normal,
|
||||
meshVertex.texCoords?.let(::vec2ToBabylon) ?: DEFAULT_UV
|
||||
meshVertex.texCoords?.let(::vec2ToThree) ?: DEFAULT_UV
|
||||
)
|
||||
|
||||
if (i >= 2) {
|
||||
if (i % 2 == if (mesh.clockwiseWinding) 0 else 1) {
|
||||
builder.addIndex(index - 2)
|
||||
builder.addIndex(index - 1)
|
||||
builder.addIndex(index)
|
||||
if (i % 2 == if (mesh.clockwiseWinding) 1 else 0) {
|
||||
builder.addIndex(group, index - 2)
|
||||
builder.addIndex(group, index - 1)
|
||||
builder.addIndex(group, index)
|
||||
} else {
|
||||
builder.addIndex(index - 2)
|
||||
builder.addIndex(index)
|
||||
builder.addIndex(index - 1)
|
||||
builder.addIndex(group, index - 2)
|
||||
builder.addIndex(group, index)
|
||||
builder.addIndex(group, index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +168,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
|
||||
for (j in boneIndices.indices) {
|
||||
builder.addBoneWeight(
|
||||
group,
|
||||
boneIndices[j],
|
||||
if (totalWeight > 0f) boneWeights[j] / totalWeight else 0f
|
||||
)
|
||||
@ -149,38 +177,41 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support multiple materials
|
||||
// builder.addGroup(
|
||||
// startIndexCount
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
private fun xjModelToVertexData(model: XjModel, matrix: Matrix) {
|
||||
private fun convertXjModel(model: XjModel, matrix: Matrix4) {
|
||||
val indexOffset = builder.vertexCount
|
||||
val normalMatrix = Matrix.Identity()
|
||||
matrix.toNormalMatrix(normalMatrix)
|
||||
val normalMatrix = Matrix3().getNormalMatrix(matrix)
|
||||
|
||||
for (vertex in model.vertices) {
|
||||
val p = vec3ToBabylon(vertex.position)
|
||||
matrix.multiply(p)
|
||||
val p = vec3ToThree(vertex.position)
|
||||
p.applyMatrix4(matrix)
|
||||
|
||||
val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
||||
normalMatrix.multiply3x3(n)
|
||||
val n = vertex.normal?.let(::vec3ToThree) ?: Vector3(0.0, 1.0, 0.0)
|
||||
n.applyMatrix3(normalMatrix)
|
||||
|
||||
val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV
|
||||
val uv = vertex.uv?.let(::vec2ToThree) ?: DEFAULT_UV
|
||||
|
||||
builder.addVertex(p, n, uv)
|
||||
}
|
||||
|
||||
var currentMatIdx: Int? = null
|
||||
var currentTextureIdx: Int? = null
|
||||
var currentSrcAlpha: Int? = null
|
||||
var currentDstAlpha: Int? = null
|
||||
|
||||
for (mesh in model.meshes) {
|
||||
val startIndexCount = builder.indexCount
|
||||
var clockwise = true
|
||||
mesh.material.textureId?.let { currentTextureIdx = it }
|
||||
mesh.material.srcAlpha?.let { currentSrcAlpha = it }
|
||||
mesh.material.dstAlpha?.let { currentDstAlpha = it }
|
||||
|
||||
val group = builder.getGroupIndex(
|
||||
currentTextureIdx,
|
||||
alpha = true,
|
||||
additiveBlending = currentSrcAlpha != 4 || currentDstAlpha != 5,
|
||||
)
|
||||
|
||||
var clockwise = false
|
||||
|
||||
for (j in 2 until mesh.indices.size) {
|
||||
val a = indexOffset + mesh.indices[j - 2]
|
||||
@ -198,8 +229,8 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
// most models.
|
||||
val normal = (pb - pa) cross (pc - pa)
|
||||
|
||||
if (!clockwise) {
|
||||
normal.negateInPlace()
|
||||
if (clockwise) {
|
||||
normal.negate()
|
||||
}
|
||||
|
||||
val oppositeCount =
|
||||
@ -212,30 +243,17 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
}
|
||||
|
||||
if (clockwise) {
|
||||
builder.addIndex(b)
|
||||
builder.addIndex(a)
|
||||
builder.addIndex(c)
|
||||
builder.addIndex(group, b)
|
||||
builder.addIndex(group, a)
|
||||
builder.addIndex(group, c)
|
||||
} else {
|
||||
builder.addIndex(a)
|
||||
builder.addIndex(b)
|
||||
builder.addIndex(c)
|
||||
builder.addIndex(group, a)
|
||||
builder.addIndex(group, b)
|
||||
builder.addIndex(group, c)
|
||||
}
|
||||
|
||||
clockwise = !clockwise
|
||||
}
|
||||
|
||||
mesh.material.textureId?.let { currentMatIdx = it }
|
||||
mesh.material.srcAlpha?.let { currentSrcAlpha = it }
|
||||
mesh.material.dstAlpha?.let { currentDstAlpha = it }
|
||||
|
||||
// TODO: support multiple materials
|
||||
// builder.addGroup(
|
||||
// start_index_count,
|
||||
// this.builder.index_count - start_index_count,
|
||||
// current_mat_idx,
|
||||
// true,
|
||||
// current_src_alpha !== 4 || current_dst_alpha !== 5,
|
||||
// );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -266,33 +284,3 @@ private class VertexHolder {
|
||||
|
||||
fun get(index: Int): List<Vertex> = buffer[index]
|
||||
}
|
||||
|
||||
private fun eulerToQuat(angles: Vec3, zxyRotationOrder: Boolean): Quaternion {
|
||||
val x = angles.x.toDouble()
|
||||
val y = angles.y.toDouble()
|
||||
val z = angles.z.toDouble()
|
||||
|
||||
val c1 = cos(x / 2)
|
||||
val c2 = cos(y / 2)
|
||||
val c3 = cos(z / 2)
|
||||
|
||||
val s1 = sin(x / 2)
|
||||
val s2 = sin(y / 2)
|
||||
val s3 = sin(z / 2)
|
||||
|
||||
return if (zxyRotationOrder) {
|
||||
Quaternion(
|
||||
s1 * c2 * c3 - c1 * s2 * s3,
|
||||
c1 * s2 * c3 + s1 * c2 * s3,
|
||||
c1 * c2 * s3 + s1 * s2 * c3,
|
||||
c1 * c2 * c3 - s1 * s2 * s3,
|
||||
)
|
||||
} else {
|
||||
Quaternion(
|
||||
s1 * c2 * c3 - c1 * s2 * s3,
|
||||
c1 * s2 * c3 + s1 * c2 * s3,
|
||||
c1 * c2 * s3 - s1 * s2 * c3,
|
||||
c1 * c2 * c3 + s1 * s2 * s3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
package world.phantasmal.web.core.rendering.conversion
|
||||
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Uint16Array
|
||||
import org.khronos.webgl.set
|
||||
import world.phantasmal.web.externals.babylon.Vector2
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.externals.babylon.VertexData
|
||||
|
||||
class VertexDataBuilder {
|
||||
private val positions = mutableListOf<Vector3>()
|
||||
private val normals = mutableListOf<Vector3>()
|
||||
private val uvs = mutableListOf<Vector2>()
|
||||
private val indices = mutableListOf<Short>()
|
||||
private val boneIndices = mutableListOf<Short>()
|
||||
private val boneWeights = mutableListOf<Float>()
|
||||
|
||||
val vertexCount: Int
|
||||
get() = positions.size
|
||||
|
||||
val indexCount: Int
|
||||
get() = indices.size
|
||||
|
||||
fun getPosition(index: Int): Vector3 =
|
||||
positions[index]
|
||||
|
||||
fun getNormal(index: Int): Vector3 =
|
||||
normals[index]
|
||||
|
||||
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2? = null) {
|
||||
positions.add(position)
|
||||
normals.add(normal)
|
||||
uv?.let { uvs.add(uv) }
|
||||
}
|
||||
|
||||
fun addIndex(index: Int) {
|
||||
indices.add(index.toShort())
|
||||
}
|
||||
|
||||
fun addBoneWeight(index: Int, weight: Float) {
|
||||
boneIndices.add(index.toShort())
|
||||
boneWeights.add(weight)
|
||||
}
|
||||
|
||||
// TODO: support multiple materials
|
||||
// fun addGroup(
|
||||
// offset: Int,
|
||||
// size: Int,
|
||||
// textureId: Int?,
|
||||
// alpha: Boolean = false,
|
||||
// additiveBlending: Boolean = false,
|
||||
// ) {
|
||||
//
|
||||
// }
|
||||
|
||||
fun build(): VertexData {
|
||||
check(this.positions.size == this.normals.size)
|
||||
check(this.uvs.isEmpty() || this.positions.size == this.uvs.size)
|
||||
|
||||
val positions = Float32Array(3 * positions.size)
|
||||
val normals = Float32Array(3 * normals.size)
|
||||
val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size)
|
||||
|
||||
for (i in this.positions.indices) {
|
||||
val pos = this.positions[i]
|
||||
positions[3 * i] = pos.x.toFloat()
|
||||
positions[3 * i + 1] = pos.y.toFloat()
|
||||
positions[3 * i + 2] = pos.z.toFloat()
|
||||
|
||||
val normal = this.normals[i]
|
||||
normals[3 * i] = normal.x.toFloat()
|
||||
normals[3 * i + 1] = normal.y.toFloat()
|
||||
normals[3 * i + 2] = normal.z.toFloat()
|
||||
|
||||
uvs?.let {
|
||||
val uv = this.uvs[i]
|
||||
uvs[2 * i] = uv.x.toFloat()
|
||||
uvs[2 * i + 1] = uv.y.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
val data = VertexData()
|
||||
data.positions = positions
|
||||
data.normals = normals
|
||||
data.uvs = uvs
|
||||
data.indices = Uint16Array(indices.toTypedArray())
|
||||
return data
|
||||
}
|
||||
}
|
@ -1,19 +1,15 @@
|
||||
package world.phantasmal.web.core.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
import kotlin.math.floor
|
||||
|
||||
class RendererWidget(
|
||||
scope: CoroutineScope,
|
||||
private val canvas: HTMLCanvasElement,
|
||||
private val renderer: Renderer,
|
||||
) : Widget(scope) {
|
||||
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-core-renderer"
|
||||
@ -28,11 +24,10 @@ class RendererWidget(
|
||||
}
|
||||
|
||||
addDisposable(size.observe { (size) ->
|
||||
canvas.width = floor(size.width).toInt()
|
||||
canvas.height = floor(size.height).toInt()
|
||||
renderer.setSize(size.width, size.height)
|
||||
})
|
||||
|
||||
append(canvas)
|
||||
append(renderer.canvas)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,506 +0,0 @@
|
||||
@file:JsModule("@babylonjs/core")
|
||||
@file:JsNonModule
|
||||
@file:Suppress("FunctionName", "unused", "CovariantEquals")
|
||||
|
||||
package world.phantasmal.web.externals.babylon
|
||||
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Uint16Array
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
|
||||
external class Vector2(x: Double, y: Double) {
|
||||
var x: Double
|
||||
var y: Double
|
||||
|
||||
fun set(x: Double, y: Double): Vector2
|
||||
fun addInPlace(otherVector: Vector2): Vector2
|
||||
fun addInPlaceFromFloats(x: Double, y: Double): Vector2
|
||||
fun subtract(otherVector: Vector2): Vector2
|
||||
fun negate(): Vector2
|
||||
fun negateInPlace(): Vector2
|
||||
fun clone(): Vector2
|
||||
fun copyFrom(source: Vector2): Vector2
|
||||
fun equals(otherVector: Vector2): Boolean
|
||||
|
||||
companion object {
|
||||
fun Zero(): Vector2
|
||||
fun Dot(left: Vector2, right: Vector2): Double
|
||||
}
|
||||
}
|
||||
|
||||
external class Vector3(x: Double, y: Double, z: Double) {
|
||||
var x: Double
|
||||
var y: Double
|
||||
var z: Double
|
||||
|
||||
fun set(x: Double, y: Double, z: Double): Vector2
|
||||
fun toQuaternion(): Quaternion
|
||||
fun add(otherVector: Vector3): Vector3
|
||||
fun addInPlace(otherVector: Vector3): Vector3
|
||||
fun addInPlaceFromFloats(x: Double, y: Double, z: Double): Vector3
|
||||
fun subtract(otherVector: Vector3): Vector3
|
||||
fun subtractInPlace(otherVector: Vector3): Vector3
|
||||
fun negate(): Vector3
|
||||
fun negateInPlace(): Vector3
|
||||
fun cross(other: Vector3): Vector3
|
||||
|
||||
/**
|
||||
* Returns a new Vector3 set with the current Vector3 coordinates multiplied by the float "scale"
|
||||
*/
|
||||
fun scale(scale: Double): Vector3
|
||||
|
||||
/**
|
||||
* Multiplies the Vector3 coordinates by the float "scale"
|
||||
*
|
||||
* @return the current updated Vector3
|
||||
*/
|
||||
fun scaleInPlace(scale: Double): Vector3
|
||||
|
||||
fun rotateByQuaternionToRef(quaternion: Quaternion, result: Vector3): Vector3
|
||||
fun clone(): Vector3
|
||||
fun copyFrom(source: Vector3): Vector3
|
||||
fun equals(otherVector: Vector3): Boolean
|
||||
|
||||
companion object {
|
||||
fun One(): Vector3
|
||||
fun Up(): Vector3
|
||||
fun Down(): Vector3
|
||||
fun Zero(): Vector3
|
||||
fun Dot(left: Vector3, right: Vector3): Double
|
||||
fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3
|
||||
fun TransformCoordinatesToRef(vector: Vector3, transformation: Matrix, result: Vector3)
|
||||
fun TransformNormal(vector: Vector3, transformation: Matrix): Vector3
|
||||
fun TransformNormalToRef(vector: Vector3, transformation: Matrix, result: Vector3)
|
||||
}
|
||||
}
|
||||
|
||||
external class Quaternion(
|
||||
x: Double = definedExternally,
|
||||
y: Double = definedExternally,
|
||||
z: Double = definedExternally,
|
||||
w: Double = definedExternally,
|
||||
) {
|
||||
/**
|
||||
* Multiplies two quaternions
|
||||
* @return a new quaternion set as the multiplication result of the current one with the given one "q1"
|
||||
*/
|
||||
fun multiply(q1: Quaternion): Quaternion
|
||||
|
||||
/**
|
||||
* Updates the current quaternion with the multiplication of itself with the given one "q1"
|
||||
* @return the current, updated quaternion
|
||||
*/
|
||||
fun multiplyInPlace(q1: Quaternion): Quaternion
|
||||
|
||||
/**
|
||||
* Sets the given "result" as the the multiplication result of the current one with the given one "q1"
|
||||
* @return the current quaternion
|
||||
*/
|
||||
fun multiplyToRef(q1: Quaternion, result: Quaternion): Quaternion
|
||||
fun toEulerAngles(): Vector3
|
||||
fun toEulerAnglesToRef(result: Vector3): Quaternion
|
||||
fun rotateByQuaternionToRef(quaternion: Quaternion, result: Vector3): Vector3
|
||||
fun clone(): Quaternion
|
||||
fun copyFrom(other: Quaternion): Quaternion
|
||||
|
||||
companion object {
|
||||
fun Identity(): Quaternion
|
||||
fun FromEulerAngles(x: Double, y: Double, z: Double): Quaternion
|
||||
fun FromEulerAnglesToRef(x: Double, y: Double, z: Double, result: Quaternion): Quaternion
|
||||
fun RotationYawPitchRoll(yaw: Double, pitch: Double, roll: Double): Quaternion
|
||||
fun Inverse(q: Quaternion): Quaternion
|
||||
fun InverseToRef(q: Quaternion, result: Quaternion): Quaternion
|
||||
}
|
||||
}
|
||||
|
||||
external class Matrix {
|
||||
fun multiply(other: Matrix): Matrix
|
||||
fun multiplyToRef(other: Matrix, result: Matrix): Matrix
|
||||
fun toNormalMatrix(ref: Matrix)
|
||||
fun copyFrom(other: Matrix): Matrix
|
||||
fun equals(value: Matrix): Boolean
|
||||
|
||||
companion object {
|
||||
val IdentityReadOnly: Matrix
|
||||
|
||||
fun Identity(): Matrix
|
||||
fun Compose(scale: Vector3, rotation: Quaternion, translation: Vector3): Matrix
|
||||
}
|
||||
}
|
||||
|
||||
external class EventState
|
||||
|
||||
external class Observable<T> {
|
||||
fun add(
|
||||
callback: (eventData: T, eventState: EventState) -> Unit,
|
||||
mask: Int = definedExternally,
|
||||
insertFirst: Boolean = definedExternally,
|
||||
scope: Any = definedExternally,
|
||||
unregisterOnFirstCall: Boolean = definedExternally,
|
||||
): Observer<T>?
|
||||
|
||||
fun remove(observer: Observer<T>?): Boolean
|
||||
|
||||
fun removeCallback(
|
||||
callback: (eventData: T, eventState: EventState) -> Unit,
|
||||
scope: Any = definedExternally,
|
||||
): Boolean
|
||||
}
|
||||
|
||||
external class Observer<T>
|
||||
|
||||
open external class ThinEngine {
|
||||
val description: String
|
||||
|
||||
/**
|
||||
* Register and execute a render loop. The engine can have more than one render function
|
||||
* @param renderFunction defines the function to continuously execute
|
||||
*/
|
||||
fun runRenderLoop(renderFunction: () -> Unit)
|
||||
|
||||
/**
|
||||
* stop executing a render loop function and remove it from the execution array
|
||||
* @param renderFunction defines the function to be removed. If not provided all functions will
|
||||
* be removed.
|
||||
*/
|
||||
fun stopRenderLoop(renderFunction: () -> Unit = definedExternally)
|
||||
fun getRenderWidth(useScreen: Boolean = definedExternally): Double
|
||||
fun getRenderHeight(useScreen: Boolean = definedExternally): Double
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
open external class Engine(
|
||||
canvasOrContext: HTMLCanvasElement?,
|
||||
antialias: Boolean = definedExternally,
|
||||
) : ThinEngine
|
||||
|
||||
external class NullEngine : Engine
|
||||
|
||||
external class Ray(origin: Vector3, direction: Vector3, length: Double = definedExternally) {
|
||||
var origin: Vector3
|
||||
var direction: Vector3
|
||||
var length: Double
|
||||
|
||||
fun intersectsPlane(plane: Plane): Double?
|
||||
|
||||
companion object {
|
||||
fun Zero(): Ray
|
||||
}
|
||||
}
|
||||
|
||||
external class PickingInfo {
|
||||
val bu: Double
|
||||
val bv: Double
|
||||
val distance: Double
|
||||
val faceId: Int
|
||||
val hit: Boolean
|
||||
val originMesh: AbstractMesh?
|
||||
val pickedMesh: AbstractMesh?
|
||||
val pickedPoint: Vector3?
|
||||
val ray: Ray?
|
||||
|
||||
fun getNormal(
|
||||
useWorldCoordinates: Boolean = definedExternally,
|
||||
useVerticesNormals: Boolean = definedExternally,
|
||||
): Vector3?
|
||||
|
||||
fun getTextureCoordinates(): Vector2?
|
||||
}
|
||||
|
||||
external class Scene(engine: Engine) {
|
||||
var useRightHandedSystem: Boolean
|
||||
var clearColor: Color4
|
||||
var pointerX: Double
|
||||
var pointerY: Double
|
||||
|
||||
fun render()
|
||||
fun addLight(light: Light)
|
||||
fun addMesh(newMesh: AbstractMesh, recursive: Boolean? = definedExternally)
|
||||
fun addTransformNode(newTransformNode: TransformNode)
|
||||
fun removeLight(toRemove: Light)
|
||||
fun removeMesh(toRemove: TransformNode, recursive: Boolean? = definedExternally)
|
||||
fun removeTransformNode(toRemove: TransformNode)
|
||||
|
||||
fun createPickingRay(
|
||||
x: Double,
|
||||
y: Double,
|
||||
world: Matrix,
|
||||
camera: Camera?,
|
||||
cameraViewSpace: Boolean = definedExternally,
|
||||
): Ray
|
||||
|
||||
fun createPickingRayToRef(
|
||||
x: Double,
|
||||
y: Double,
|
||||
world: Matrix,
|
||||
result: Ray,
|
||||
camera: Camera?,
|
||||
cameraViewSpace: Boolean = definedExternally,
|
||||
): Scene
|
||||
|
||||
fun createPickingRayInCameraSpaceToRef(
|
||||
x: Double,
|
||||
y: Double,
|
||||
result: Ray,
|
||||
camera: Camera = definedExternally,
|
||||
): Scene
|
||||
|
||||
fun pick(
|
||||
x: Double,
|
||||
y: Double,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
fastCheck: Boolean = definedExternally,
|
||||
camera: Camera? = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): PickingInfo?
|
||||
|
||||
fun pickWithRay(
|
||||
ray: Ray,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
fastCheck: Boolean = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): PickingInfo?
|
||||
|
||||
/**
|
||||
* @param x X position on screen
|
||||
* @param y Y position on screen
|
||||
* @param predicate Predicate function used to determine eligible meshes. Can be set to null. In this case, a mesh must be enabled, visible and with isPickable set to true
|
||||
*/
|
||||
fun multiPick(
|
||||
x: Double,
|
||||
y: Double,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
camera: Camera = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): Array<PickingInfo>?
|
||||
|
||||
fun multiPickWithRay(
|
||||
ray: Ray,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): Array<PickingInfo>?
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
open external class Node {
|
||||
var metadata: Any?
|
||||
var parent: Node?
|
||||
|
||||
fun isEnabled(checkAncestors: Boolean = definedExternally): Boolean
|
||||
fun setEnabled(value: Boolean)
|
||||
fun getWorldMatrix(): Matrix
|
||||
|
||||
/**
|
||||
* Releases resources associated with this node.
|
||||
* @param doNotRecurse Set to true to not recurse into each children (recurse into each children by default)
|
||||
* @param disposeMaterialAndTextures Set to true to also dispose referenced materials and textures (false by default)
|
||||
*/
|
||||
fun dispose(
|
||||
doNotRecurse: Boolean = definedExternally,
|
||||
disposeMaterialAndTextures: Boolean = definedExternally,
|
||||
)
|
||||
}
|
||||
|
||||
open external class Camera : Node {
|
||||
var minZ: Double
|
||||
var maxZ: Double
|
||||
val absoluteRotation: Quaternion
|
||||
val onProjectionMatrixChangedObservable: Observable<Camera>
|
||||
val onViewMatrixChangedObservable: Observable<Camera>
|
||||
val onAfterCheckInputsObservable: Observable<Camera>
|
||||
|
||||
fun getViewMatrix(force: Boolean = definedExternally): Matrix
|
||||
fun getProjectionMatrix(force: Boolean = definedExternally): Matrix
|
||||
fun getTransformationMatrix(): Matrix
|
||||
fun attachControl(noPreventDefault: Boolean = definedExternally)
|
||||
fun detachControl()
|
||||
fun storeState(): Camera
|
||||
fun restoreState(): Boolean
|
||||
}
|
||||
|
||||
open external class TargetCamera : Camera {
|
||||
var target: Vector3
|
||||
}
|
||||
|
||||
/**
|
||||
* @param setActiveOnSceneIfNoneActive default true
|
||||
*/
|
||||
external class ArcRotateCamera(
|
||||
name: String,
|
||||
alpha: Double,
|
||||
beta: Double,
|
||||
radius: Double,
|
||||
target: Vector3,
|
||||
scene: Scene,
|
||||
setActiveOnSceneIfNoneActive: Boolean = definedExternally,
|
||||
) : TargetCamera {
|
||||
var alpha: Double
|
||||
var beta: Double
|
||||
var radius: Double
|
||||
var inertia: Double
|
||||
var angularSensibilityX: Double
|
||||
var angularSensibilityY: Double
|
||||
var panningInertia: Double
|
||||
var panningSensibility: Double
|
||||
var panningAxis: Vector3
|
||||
var pinchDeltaPercentage: Double
|
||||
var wheelDeltaPercentage: Double
|
||||
var lowerBetaLimit: Double
|
||||
val inputs: ArcRotateCameraInputsManager
|
||||
|
||||
fun attachControl(
|
||||
element: HTMLCanvasElement,
|
||||
noPreventDefault: Boolean,
|
||||
useCtrlForPanning: Boolean,
|
||||
panningMouseButton: Int,
|
||||
)
|
||||
}
|
||||
|
||||
open external class CameraInputsManager<TCamera : Camera> {
|
||||
fun attachElement(noPreventDefault: Boolean = definedExternally)
|
||||
fun detachElement(disconnect: Boolean = definedExternally)
|
||||
}
|
||||
|
||||
external class ArcRotateCameraInputsManager : CameraInputsManager<ArcRotateCamera>
|
||||
|
||||
abstract external class Light : Node
|
||||
|
||||
external class HemisphericLight(name: String, direction: Vector3, scene: Scene) : Light {
|
||||
var direction: Vector3
|
||||
}
|
||||
|
||||
open external class TransformNode(
|
||||
name: String,
|
||||
scene: Scene? = definedExternally,
|
||||
isPure: Boolean = definedExternally,
|
||||
) : Node {
|
||||
var position: Vector3
|
||||
var rotation: Vector3
|
||||
var rotationQuaternion: Quaternion?
|
||||
val absoluteRotation: Quaternion
|
||||
var scaling: Vector3
|
||||
|
||||
fun locallyTranslate(vector3: Vector3): TransformNode
|
||||
}
|
||||
|
||||
abstract external class AbstractMesh : TransformNode {
|
||||
var showBoundingBox: Boolean
|
||||
|
||||
fun getBoundingInfo(): BoundingInfo
|
||||
}
|
||||
|
||||
external class Mesh(
|
||||
name: String,
|
||||
scene: Scene? = definedExternally,
|
||||
parent: Node? = definedExternally,
|
||||
source: Mesh? = definedExternally,
|
||||
doNotCloneChildren: Boolean = definedExternally,
|
||||
clonePhysicsImpostor: Boolean = definedExternally,
|
||||
) : AbstractMesh {
|
||||
fun createInstance(name: String): InstancedMesh
|
||||
fun bakeCurrentTransformIntoVertices(
|
||||
bakeIndependenlyOfChildren: Boolean = definedExternally,
|
||||
): Mesh
|
||||
}
|
||||
|
||||
external class InstancedMesh : AbstractMesh
|
||||
|
||||
external class BoundingInfo {
|
||||
val boundingBox: BoundingBox
|
||||
val boundingSphere: BoundingSphere
|
||||
}
|
||||
|
||||
external class BoundingBox {
|
||||
val center: Vector3
|
||||
val centerWorld: Vector3
|
||||
val directions: Array<Vector3>
|
||||
val extendSize: Vector3
|
||||
val extendSizeWorld: Vector3
|
||||
val maximum: Vector3
|
||||
val maximumWorld: Vector3
|
||||
val minimum: Vector3
|
||||
val minimumWorld: Vector3
|
||||
val vectors: Array<Vector3>
|
||||
val vectorsWorld: Array<Vector3>
|
||||
}
|
||||
|
||||
external class BoundingSphere {
|
||||
val center: Vector3
|
||||
val centerWorld: Vector3
|
||||
val maximum: Vector3
|
||||
val minimum: Vector3
|
||||
val radius: Double
|
||||
val radiusWorld: Double
|
||||
}
|
||||
|
||||
external class MeshBuilder {
|
||||
companion object {
|
||||
interface CreateCylinderOptions {
|
||||
var height: Double
|
||||
var diameterTop: Double
|
||||
var diameterBottom: Double
|
||||
var diameter: Double
|
||||
var tessellation: Double
|
||||
var subdivisions: Double
|
||||
var arc: Double
|
||||
}
|
||||
|
||||
fun CreateCylinder(
|
||||
name: String,
|
||||
options: CreateCylinderOptions,
|
||||
scene: Scene? = definedExternally,
|
||||
): Mesh
|
||||
}
|
||||
}
|
||||
|
||||
external class Plane(a: Double, b: Double, c: Double, d: Double) {
|
||||
var normal: Vector3
|
||||
var d: Double
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Note : the vector "normal" is updated because normalized.
|
||||
*/
|
||||
fun FromPositionAndNormal(origin: Vector3, normal: Vector3): Plane
|
||||
}
|
||||
}
|
||||
|
||||
external class VertexData {
|
||||
var positions: Float32Array? // number[] | Float32Array
|
||||
var normals: Float32Array? // number[] | Float32Array
|
||||
var uvs: Float32Array? // number[] | Float32Array
|
||||
var indices: Uint16Array? // number[] | Int32Array | Uint32Array | Uint16Array
|
||||
|
||||
fun applyToMesh(mesh: Mesh, updatable: Boolean = definedExternally): VertexData
|
||||
}
|
||||
|
||||
external class Color3(
|
||||
r: Double = definedExternally,
|
||||
g: Double = definedExternally,
|
||||
b: Double = definedExternally,
|
||||
) {
|
||||
var r: Double
|
||||
var g: Double
|
||||
var b: Double
|
||||
}
|
||||
|
||||
external class Color4(
|
||||
r: Double = definedExternally,
|
||||
g: Double = definedExternally,
|
||||
b: Double = definedExternally,
|
||||
a: Double = definedExternally,
|
||||
) {
|
||||
var r: Double
|
||||
var g: Double
|
||||
var b: Double
|
||||
var a: Double
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a new Color4 from integer values (< 256)
|
||||
*/
|
||||
fun FromInts(r: Int, g: Int, b: Int, a: Int): Color4
|
||||
}
|
||||
}
|
23
web/src/main/kotlin/world/phantasmal/web/externals/three/OrbitControls.kt
vendored
Normal file
23
web/src/main/kotlin/world/phantasmal/web/externals/three/OrbitControls.kt
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
@file:JsModule("three/examples/jsm/controls/OrbitControls")
|
||||
@file:JsNonModule
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package world.phantasmal.web.externals.three
|
||||
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
external interface OrbitControlsMouseButtons {
|
||||
var LEFT: MOUSE
|
||||
var MIDDLE: MOUSE
|
||||
var RIGHT: MOUSE
|
||||
}
|
||||
|
||||
external class OrbitControls(`object`: Camera, domElement: HTMLElement = definedExternally) {
|
||||
var enabled: Boolean
|
||||
var target: Vector3
|
||||
var screenSpacePanning: Boolean
|
||||
|
||||
var mouseButtons: OrbitControlsMouseButtons
|
||||
|
||||
fun update(): Boolean
|
||||
}
|
608
web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt
vendored
Normal file
608
web/src/main/kotlin/world/phantasmal/web/externals/three/three.kt
vendored
Normal file
@ -0,0 +1,608 @@
|
||||
@file:JsModule("three")
|
||||
@file:JsNonModule
|
||||
@file:Suppress("unused", "ClassName", "CovariantEquals")
|
||||
|
||||
package world.phantasmal.web.externals.three
|
||||
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Uint16Array
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
|
||||
external interface Vector
|
||||
|
||||
external class Vector2(x: Double = definedExternally, y: Double = definedExternally) : Vector {
|
||||
var x: Double
|
||||
var y: Double
|
||||
|
||||
/**
|
||||
* Sets value of this vector.
|
||||
*/
|
||||
fun set(x: Double, y: Double): Vector2
|
||||
|
||||
/**
|
||||
* Copies value of v to this vector.
|
||||
*/
|
||||
fun copy(v: Vector2): Vector2
|
||||
|
||||
/**
|
||||
* Checks for strict equality of this vector and v.
|
||||
*/
|
||||
fun equals(v: Vector2): Boolean
|
||||
}
|
||||
|
||||
external class Vector3(
|
||||
x: Double = definedExternally,
|
||||
y: Double = definedExternally,
|
||||
z: Double = definedExternally,
|
||||
) : Vector {
|
||||
var x: Double
|
||||
var y: Double
|
||||
var z: Double
|
||||
|
||||
/**
|
||||
* Sets value of this vector.
|
||||
*/
|
||||
fun set(x: Double, y: Double, z: Double): Vector3
|
||||
|
||||
fun clone(): Vector3
|
||||
|
||||
/**
|
||||
* Copies value of v to this vector.
|
||||
*/
|
||||
fun copy(v: Vector3): Vector3
|
||||
|
||||
/**
|
||||
* Checks for strict equality of this vector and v.
|
||||
*/
|
||||
fun equals(v: Vector3): Boolean
|
||||
|
||||
/**
|
||||
* Adds [v] to this vector.
|
||||
*/
|
||||
fun add(v: Vector3): Vector3
|
||||
|
||||
/**
|
||||
* Subtracts [v] from this vector.
|
||||
*/
|
||||
fun sub(v: Vector3): Vector3
|
||||
|
||||
/**
|
||||
* Multiplies this vector by scalar s.
|
||||
*/
|
||||
fun multiplyScalar(s: Double): Vector3
|
||||
|
||||
/**
|
||||
* Inverts this vector.
|
||||
*/
|
||||
fun negate(): Vector3
|
||||
|
||||
/**
|
||||
* Computes dot product of this vector and v.
|
||||
*/
|
||||
fun dot(v: Vector3): Double
|
||||
|
||||
fun length(): Double
|
||||
|
||||
/**
|
||||
* Sets this vector to cross product of itself and [v].
|
||||
*/
|
||||
fun cross(v: Vector3): Vector3
|
||||
|
||||
fun applyEuler(euler: Euler): Vector3
|
||||
fun applyMatrix3(m: Matrix3): Vector3
|
||||
fun applyNormalMatrix(m: Matrix3): Vector3
|
||||
fun applyMatrix4(m: Matrix4): Vector3
|
||||
fun applyQuaternion(q: Quaternion): Vector3
|
||||
}
|
||||
|
||||
external class Quaternion(
|
||||
x: Double = definedExternally,
|
||||
y: Double = definedExternally,
|
||||
z: Double = definedExternally,
|
||||
w: Double = definedExternally,
|
||||
) {
|
||||
fun setFromEuler(euler: Euler): Quaternion
|
||||
|
||||
/**
|
||||
* Inverts this quaternion.
|
||||
*/
|
||||
fun inverse(): Quaternion
|
||||
|
||||
/**
|
||||
* Multiplies this quaternion by [q].
|
||||
*/
|
||||
fun multiply(q: Quaternion): Quaternion
|
||||
}
|
||||
|
||||
external class Euler(
|
||||
x: Double = definedExternally,
|
||||
y: Double = definedExternally,
|
||||
z: Double = definedExternally,
|
||||
order: String = definedExternally,
|
||||
) {
|
||||
var x: Double
|
||||
var y: Double
|
||||
var z: Double
|
||||
|
||||
fun set(x: Double, y: Double, z: Double, order: String = definedExternally): Euler
|
||||
fun copy(euler: Euler): Euler
|
||||
fun setFromQuaternion(q: Quaternion, order: String = definedExternally): Euler
|
||||
}
|
||||
|
||||
external class Matrix3 {
|
||||
fun getNormalMatrix(matrix4: Matrix4): Matrix3
|
||||
}
|
||||
|
||||
external class Matrix4 {
|
||||
fun compose(translation: Vector3, rotation: Quaternion, scale: Vector3): Matrix4
|
||||
|
||||
fun premultiply(m: Matrix4): Matrix4
|
||||
}
|
||||
|
||||
open external class EventDispatcher
|
||||
|
||||
external interface Renderer {
|
||||
val domElement: HTMLCanvasElement
|
||||
|
||||
fun render(scene: Object3D, camera: Camera)
|
||||
|
||||
fun setSize(width: Double, height: Double, updateStyle: Boolean = definedExternally)
|
||||
}
|
||||
|
||||
external interface WebGLRendererParameters {
|
||||
var alpha: Boolean
|
||||
var premultipliedAlpha: Boolean
|
||||
var antialias: Boolean
|
||||
}
|
||||
|
||||
external class WebGLRenderer(parameters: WebGLRendererParameters = definedExternally) : Renderer {
|
||||
override val domElement: HTMLCanvasElement
|
||||
|
||||
override fun render(scene: Object3D, camera: Camera)
|
||||
|
||||
override fun setSize(width: Double, height: Double, updateStyle: Boolean)
|
||||
|
||||
fun setPixelRatio(value: Double)
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
open external class Object3D {
|
||||
/**
|
||||
* Optional name of the object (doesn't need to be unique).
|
||||
*/
|
||||
var name: String
|
||||
|
||||
var parent: Object3D?
|
||||
|
||||
var children: Array<Object3D>
|
||||
|
||||
/**
|
||||
* Object's local position.
|
||||
*/
|
||||
val position: Vector3
|
||||
|
||||
/**
|
||||
* Object's local rotation (Euler angles), in radians.
|
||||
*/
|
||||
val rotation: Euler
|
||||
|
||||
/**
|
||||
* Global rotation.
|
||||
*/
|
||||
val quaternion: Quaternion
|
||||
|
||||
/**
|
||||
* Object's local scale.
|
||||
*/
|
||||
val scale: Vector3
|
||||
|
||||
/**
|
||||
* Local transform.
|
||||
*/
|
||||
var matrix: Matrix4
|
||||
|
||||
/**
|
||||
* An object that can be used to store custom data about the Object3d. It should not hold references to functions as these will not be cloned.
|
||||
*/
|
||||
var userData: Any
|
||||
|
||||
fun add(vararg `object`: Object3D): Object3D
|
||||
fun remove(vararg `object`: Object3D): Object3D
|
||||
fun clear(): Object3D
|
||||
|
||||
/**
|
||||
* Updates local transform.
|
||||
*/
|
||||
fun updateMatrix()
|
||||
|
||||
/**
|
||||
* Updates global transform of the object and its children.
|
||||
*/
|
||||
fun updateMatrixWorld(force: Boolean = definedExternally)
|
||||
|
||||
fun clone(recursive: Boolean = definedExternally): Object3D
|
||||
}
|
||||
|
||||
external class Group : Object3D
|
||||
|
||||
open external class Mesh(
|
||||
geometry: Geometry = definedExternally,
|
||||
material: Material = definedExternally,
|
||||
) : Object3D {
|
||||
constructor(
|
||||
geometry: Geometry,
|
||||
material: Array<Material>,
|
||||
)
|
||||
|
||||
constructor(
|
||||
geometry: BufferGeometry = definedExternally,
|
||||
material: Material = definedExternally,
|
||||
)
|
||||
|
||||
constructor(
|
||||
geometry: BufferGeometry,
|
||||
material: Array<Material>,
|
||||
)
|
||||
|
||||
var geometry: Any /* Geometry | BufferGeometry */
|
||||
var material: Any /* Material | Material[] */
|
||||
|
||||
fun translateY(distance: Double): Mesh
|
||||
}
|
||||
|
||||
external class InstancedMesh(
|
||||
geometry: Geometry,
|
||||
material: Material,
|
||||
count: Int,
|
||||
) : Mesh {
|
||||
constructor(
|
||||
geometry: Geometry,
|
||||
material: Array<Material>,
|
||||
count: Int,
|
||||
)
|
||||
|
||||
constructor(
|
||||
geometry: BufferGeometry,
|
||||
material: Material,
|
||||
count: Int,
|
||||
)
|
||||
|
||||
constructor(
|
||||
geometry: BufferGeometry,
|
||||
material: Array<Material>,
|
||||
count: Int,
|
||||
)
|
||||
|
||||
var count: Int
|
||||
var instanceMatrix: BufferAttribute
|
||||
|
||||
fun getMatrixAt(index: Int, matrix: Matrix4)
|
||||
fun setMatrixAt(index: Int, matrix: Matrix4)
|
||||
}
|
||||
|
||||
external class Scene : Object3D {
|
||||
var background: dynamic /* null | Color | Texture | WebGLCubeRenderTarget */
|
||||
}
|
||||
|
||||
open external class Camera : Object3D
|
||||
|
||||
external class PerspectiveCamera(
|
||||
fov: Double = definedExternally,
|
||||
aspect: Double = definedExternally,
|
||||
near: Double = definedExternally,
|
||||
far: Double = definedExternally,
|
||||
) : Camera {
|
||||
var fov: Double
|
||||
var aspect: Double
|
||||
var near: Double
|
||||
var far: Double
|
||||
|
||||
/**
|
||||
* Updates the camera projection matrix. Must be called after change of parameters.
|
||||
*/
|
||||
fun updateProjectionMatrix()
|
||||
}
|
||||
|
||||
external class OrthographicCamera(
|
||||
left: Double,
|
||||
right: Double,
|
||||
top: Double,
|
||||
bottom: Double,
|
||||
near: Double = definedExternally,
|
||||
far: Double = definedExternally,
|
||||
) : Camera {
|
||||
/**
|
||||
* Camera frustum left plane.
|
||||
*/
|
||||
var left: Double
|
||||
|
||||
/**
|
||||
* Camera frustum right plane.
|
||||
*/
|
||||
var right: Double
|
||||
|
||||
/**
|
||||
* Camera frustum top plane.
|
||||
*/
|
||||
var top: Double
|
||||
|
||||
/**
|
||||
* Camera frustum bottom plane.
|
||||
*/
|
||||
var bottom: Double
|
||||
|
||||
/**
|
||||
* Camera frustum near plane.
|
||||
*/
|
||||
var near: Double
|
||||
|
||||
/**
|
||||
* Camera frustum far plane.
|
||||
*/
|
||||
var far: Double
|
||||
|
||||
/**
|
||||
* Updates the camera projection matrix. Must be called after change of parameters.
|
||||
*/
|
||||
fun updateProjectionMatrix()
|
||||
}
|
||||
|
||||
open external class Light : Object3D
|
||||
|
||||
external class HemisphereLight(
|
||||
skyColor: Color = definedExternally,
|
||||
groundColor: Color = definedExternally,
|
||||
intensity: Double = definedExternally,
|
||||
) : Light {
|
||||
constructor(
|
||||
skyColor: Int = definedExternally,
|
||||
groundColor: Int = definedExternally,
|
||||
intensity: Double = definedExternally,
|
||||
)
|
||||
|
||||
constructor(
|
||||
skyColor: String = definedExternally,
|
||||
groundColor: String = definedExternally,
|
||||
intensity: Double = definedExternally,
|
||||
)
|
||||
}
|
||||
|
||||
external class Color(r: Double, g: Double, b: Double) {
|
||||
constructor(color: Color)
|
||||
constructor(color: String)
|
||||
constructor(color: Int)
|
||||
}
|
||||
|
||||
open external class Geometry : EventDispatcher {
|
||||
/**
|
||||
* Array of face UV layers.
|
||||
* Each UV layer is an array of UV matching order and number of vertices in faces.
|
||||
* To signal an update in this array, Geometry.uvsNeedUpdate needs to be set to true.
|
||||
*/
|
||||
var faceVertexUvs: Array<Array<Array<Vector2>>>
|
||||
|
||||
fun translate(x: Double, y: Double, z: Double): Geometry
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
external class PlaneGeometry(
|
||||
width: Double = definedExternally,
|
||||
height: Double = definedExternally,
|
||||
widthSegments: Double = definedExternally,
|
||||
heightSegments: Double = definedExternally,
|
||||
) : Geometry
|
||||
|
||||
open external class BufferGeometry : EventDispatcher {
|
||||
var boundingBox: Box3?
|
||||
|
||||
fun setIndex(index: BufferAttribute?)
|
||||
fun setIndex(index: Array<Double>?)
|
||||
|
||||
fun setAttribute(name: String, attribute: BufferAttribute): BufferGeometry
|
||||
fun setAttribute(name: String, attribute: InterleavedBufferAttribute): BufferGeometry
|
||||
|
||||
fun addGroup(start: Int, count: Int, materialIndex: Int = definedExternally)
|
||||
|
||||
fun translate(x: Double, y: Double, z: Double): BufferGeometry
|
||||
|
||||
fun computeBoundingBox()
|
||||
fun computeBoundingSphere()
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
external class CylinderBufferGeometry(
|
||||
radiusTop: Double = definedExternally,
|
||||
radiusBottom: Double = definedExternally,
|
||||
height: Double = definedExternally,
|
||||
radialSegments: Int = definedExternally,
|
||||
heightSegments: Int = definedExternally,
|
||||
openEnded: Boolean = definedExternally,
|
||||
thetaStart: Double = definedExternally,
|
||||
thetaLength: Double = definedExternally,
|
||||
) : BufferGeometry
|
||||
|
||||
open external class BufferAttribute {
|
||||
var needsUpdate: Boolean
|
||||
|
||||
fun copyAt(index1: Int, attribute: BufferAttribute, index2: Int): BufferAttribute
|
||||
}
|
||||
|
||||
external class Uint16BufferAttribute(
|
||||
array: Uint16Array,
|
||||
itemSize: Int,
|
||||
normalize: Boolean = definedExternally,
|
||||
) : BufferAttribute
|
||||
|
||||
external class Float32BufferAttribute(
|
||||
array: Float32Array,
|
||||
itemSize: Int,
|
||||
normalize: Boolean = definedExternally,
|
||||
) : BufferAttribute
|
||||
|
||||
external class InterleavedBufferAttribute
|
||||
|
||||
external interface Side
|
||||
external object FrontSide : Side
|
||||
external object BackSide : Side
|
||||
external object DoubleSide : Side
|
||||
|
||||
external interface Blending
|
||||
external object NoBlending : Blending
|
||||
external object NormalBlending : Blending
|
||||
external object AdditiveBlending : Blending
|
||||
external object SubtractiveBlending : Blending
|
||||
external object MultiplyBlending : Blending
|
||||
external object CustomBlending : Blending
|
||||
|
||||
external interface MaterialParameters {
|
||||
var alphaTest: Double
|
||||
var blending: Blending
|
||||
var side: Side
|
||||
var transparent: Boolean
|
||||
}
|
||||
|
||||
open external class Material : EventDispatcher {
|
||||
/**
|
||||
* This disposes the material. Textures of a material don't get disposed. These needs to be disposed by [Texture].
|
||||
*/
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
external interface MeshBasicMaterialParameters : MaterialParameters {
|
||||
var color: Color
|
||||
var map: Texture?
|
||||
var skinning: Boolean
|
||||
}
|
||||
|
||||
external class MeshBasicMaterial(
|
||||
parameters: MeshBasicMaterialParameters = definedExternally,
|
||||
) : Material {
|
||||
var map: Texture?
|
||||
}
|
||||
|
||||
external interface MeshLambertMaterialParameters : MaterialParameters {
|
||||
var skinning: Boolean
|
||||
}
|
||||
|
||||
external class MeshLambertMaterial(
|
||||
parameters: MeshLambertMaterialParameters = definedExternally,
|
||||
) : Material
|
||||
|
||||
open external class Texture : EventDispatcher {
|
||||
var needsUpdate: Boolean
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
external interface Mapping
|
||||
external object UVMapping : Mapping
|
||||
external object CubeReflectionMapping : Mapping
|
||||
external object CubeRefractionMapping : Mapping
|
||||
external object EquirectangularReflectionMapping : Mapping
|
||||
external object EquirectangularRefractionMapping : Mapping
|
||||
external object CubeUVReflectionMapping : Mapping
|
||||
external object CubeUVRefractionMapping : Mapping
|
||||
|
||||
external interface Wrapping
|
||||
external object RepeatWrapping : Wrapping
|
||||
external object ClampToEdgeWrapping : Wrapping
|
||||
external object MirroredRepeatWrapping : Wrapping
|
||||
|
||||
external interface TextureFilter
|
||||
external object NearestFilter : TextureFilter
|
||||
external object NearestMipmapNearestFilter : TextureFilter
|
||||
external object NearestMipMapNearestFilter : TextureFilter
|
||||
external object NearestMipmapLinearFilter : TextureFilter
|
||||
external object NearestMipMapLinearFilter : TextureFilter
|
||||
external object LinearFilter : TextureFilter
|
||||
external object LinearMipmapNearestFilter : TextureFilter
|
||||
external object LinearMipMapNearestFilter : TextureFilter
|
||||
external object LinearMipmapLinearFilter : TextureFilter
|
||||
external object LinearMipMapLinearFilter : TextureFilter
|
||||
|
||||
external interface TextureDataType
|
||||
external object UnsignedByteType : TextureDataType
|
||||
external object ByteType : TextureDataType
|
||||
external object ShortType : TextureDataType
|
||||
external object UnsignedShortType : TextureDataType
|
||||
external object IntType : TextureDataType
|
||||
external object UnsignedIntType : TextureDataType
|
||||
external object FloatType : TextureDataType
|
||||
external object HalfFloatType : TextureDataType
|
||||
external object UnsignedShort4444Type : TextureDataType
|
||||
external object UnsignedShort5551Type : TextureDataType
|
||||
external object UnsignedShort565Type : TextureDataType
|
||||
external object UnsignedInt248Type : TextureDataType
|
||||
|
||||
// DDS / ST3C Compressed texture formats
|
||||
external interface CompressedPixelFormat
|
||||
external object RGB_S3TC_DXT1_Format : CompressedPixelFormat
|
||||
external object RGBA_S3TC_DXT1_Format : CompressedPixelFormat
|
||||
external object RGBA_S3TC_DXT3_Format : CompressedPixelFormat
|
||||
external object RGBA_S3TC_DXT5_Format : CompressedPixelFormat
|
||||
|
||||
external interface TextureEncoding
|
||||
external object LinearEncoding : TextureEncoding
|
||||
external object sRGBEncoding : TextureEncoding
|
||||
external object GammaEncoding : TextureEncoding
|
||||
external object RGBEEncoding : TextureEncoding
|
||||
external object LogLuvEncoding : TextureEncoding
|
||||
external object RGBM7Encoding : TextureEncoding
|
||||
external object RGBM16Encoding : TextureEncoding
|
||||
external object RGBDEncoding : TextureEncoding
|
||||
|
||||
external class CompressedTexture(
|
||||
mipmaps: Array<dynamic>, /* Should have data, height and width. */
|
||||
width: Int,
|
||||
height: Int,
|
||||
format: CompressedPixelFormat = definedExternally,
|
||||
type: TextureDataType = definedExternally,
|
||||
mapping: Mapping = definedExternally,
|
||||
wrapS: Wrapping = definedExternally,
|
||||
wrapT: Wrapping = definedExternally,
|
||||
magFilter: TextureFilter = definedExternally,
|
||||
minFilter: TextureFilter = definedExternally,
|
||||
anisotropy: Double = definedExternally,
|
||||
encoding: TextureEncoding = definedExternally,
|
||||
) : Texture
|
||||
|
||||
external class Box3(min: Vector3 = definedExternally, max: Vector3 = definedExternally) {
|
||||
var min: Vector3
|
||||
var max: Vector3
|
||||
}
|
||||
|
||||
external enum class MOUSE {
|
||||
LEFT,
|
||||
MIDDLE,
|
||||
RIGHT,
|
||||
ROTATE,
|
||||
DOLLY,
|
||||
PAN,
|
||||
}
|
||||
|
||||
external class Raycaster(
|
||||
origin: Vector3 = definedExternally,
|
||||
direction: Vector3 = definedExternally,
|
||||
near: Double = definedExternally,
|
||||
far: Double = definedExternally,
|
||||
) {
|
||||
/**
|
||||
* Updates the ray with a new origin and direction.
|
||||
* @param coords 2D coordinates of the mouse, in normalized device coordinates (NDC)---X and Y components should be between -1 and 1.
|
||||
* @param camera camera from which the ray should originate
|
||||
*/
|
||||
fun setFromCamera(coords: Vector2, camera: Camera)
|
||||
}
|
||||
|
||||
external interface Intersection {
|
||||
var distance: Double
|
||||
var distanceToRay: Double?
|
||||
var point: Vector3
|
||||
var index: Double?
|
||||
var `object`: Object3D
|
||||
var uv: Vector2?
|
||||
var instanceId: Int?
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
package world.phantasmal.web.questEditor
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.core.PwTool
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.questEditor.controllers.*
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
@ -25,20 +23,15 @@ import world.phantasmal.webui.widgets.Widget
|
||||
class QuestEditor(
|
||||
private val assetLoader: AssetLoader,
|
||||
private val uiStore: UiStore,
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
private val createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : DisposableContainer(), PwTool {
|
||||
override val toolType = PwToolType.QuestEditor
|
||||
|
||||
override fun initialize(scope: CoroutineScope): Widget {
|
||||
// Renderer
|
||||
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
|
||||
canvas.style.outline = "none"
|
||||
val renderer = addDisposable(QuestRenderer(canvas, createEngine(canvas)))
|
||||
|
||||
// Asset Loaders
|
||||
val questLoader = addDisposable(QuestLoader(scope, assetLoader))
|
||||
val areaAssetLoader = addDisposable(AreaAssetLoader(scope, assetLoader, renderer.scene))
|
||||
val entityAssetLoader = addDisposable(EntityAssetLoader(scope, assetLoader, renderer.scene))
|
||||
val areaAssetLoader = addDisposable(AreaAssetLoader(scope, assetLoader))
|
||||
val entityAssetLoader = addDisposable(EntityAssetLoader(scope, assetLoader))
|
||||
|
||||
// Stores
|
||||
val areaStore = addDisposable(AreaStore(scope, areaAssetLoader))
|
||||
@ -57,6 +50,8 @@ class QuestEditor(
|
||||
val assemblyEditorController = addDisposable(AssemblyEditorController(assemblyEditorStore))
|
||||
|
||||
// Rendering
|
||||
// Renderer
|
||||
val renderer = addDisposable(QuestRenderer(createThreeRenderer))
|
||||
addDisposables(
|
||||
QuestEditorMeshManager(
|
||||
scope,
|
||||
@ -75,7 +70,7 @@ class QuestEditor(
|
||||
{ s -> QuestInfoWidget(s, questInfoController) },
|
||||
{ s -> NpcCountsWidget(s, npcCountsController) },
|
||||
{ s -> EntityInfoWidget(s, entityInfoController) },
|
||||
{ s -> QuestEditorRendererWidget(s, canvas, renderer) },
|
||||
{ s -> QuestEditorRendererWidget(s, renderer) },
|
||||
{ s -> AssemblyEditorWidget(s, assemblyEditorController) },
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
package world.phantasmal.web.questEditor.actions
|
||||
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.externals.three.Euler
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
|
||||
class RotateEntityAction(
|
||||
private val setSelectedEntity: (QuestEntityModel<*, *>) -> Unit,
|
||||
private val entity: QuestEntityModel<*, *>,
|
||||
private val newRotation: Vector3,
|
||||
private val oldRotation: Vector3,
|
||||
private val newRotation: Euler,
|
||||
private val oldRotation: Euler,
|
||||
private val world: Boolean,
|
||||
) : Action {
|
||||
override val description: String = "Rotate ${entity.type.simpleName}"
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.questEditor.actions
|
||||
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.SectionModel
|
||||
|
||||
|
@ -4,7 +4,9 @@ import world.phantasmal.core.math.degToRad
|
||||
import world.phantasmal.core.math.radToDeg
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.core.euler
|
||||
import world.phantasmal.web.externals.three.Euler
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
import world.phantasmal.web.questEditor.actions.RotateEntityAction
|
||||
import world.phantasmal.web.questEditor.actions.TranslateEntityAction
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
@ -32,12 +34,14 @@ class EntityInfoController(private val store: QuestEditorStore) : Controller() {
|
||||
|
||||
val waveHidden: Val<Boolean> = store.selectedEntity.map { it !is QuestNpcModel }
|
||||
|
||||
private val pos: Val<Vector3> = store.selectedEntity.flatMap { it?.position ?: DEFAULT_VECTOR }
|
||||
private val pos: Val<Vector3> =
|
||||
store.selectedEntity.flatMap { it?.position ?: DEFAULT_POSITION }
|
||||
val posX: Val<Double> = pos.map { it.x }
|
||||
val posY: Val<Double> = pos.map { it.y }
|
||||
val posZ: Val<Double> = pos.map { it.z }
|
||||
|
||||
private val rot: Val<Vector3> = store.selectedEntity.flatMap { it?.rotation ?: DEFAULT_VECTOR }
|
||||
private val rot: Val<Euler> =
|
||||
store.selectedEntity.flatMap { it?.rotation ?: DEFAULT_ROTATION }
|
||||
val rotX: Val<Double> = rot.map { radToDeg(it.x) }
|
||||
val rotY: Val<Double> = rot.map { radToDeg(it.y) }
|
||||
val rotZ: Val<Double> = rot.map { radToDeg(it.z) }
|
||||
@ -104,13 +108,14 @@ class EntityInfoController(private val store: QuestEditorStore) : Controller() {
|
||||
store.executeAction(RotateEntityAction(
|
||||
setSelectedEntity = store::setSelectedEntity,
|
||||
entity,
|
||||
Vector3(x, y, z),
|
||||
euler(x, y, z),
|
||||
entity.rotation.value,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VECTOR = value(Vector3.Zero())
|
||||
private val DEFAULT_POSITION = value(Vector3(0.0, 0.0, 0.0))
|
||||
private val DEFAULT_ROTATION = value(euler(0.0, 0.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.loading
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
@ -10,67 +9,71 @@ import world.phantasmal.lib.fileFormats.RenderObject
|
||||
import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry
|
||||
import world.phantasmal.lib.fileFormats.parseAreaGeometry
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.web.core.euler
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.conversion.VertexDataBuilder
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexDataBuilder
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon
|
||||
import world.phantasmal.web.externals.babylon.Mesh
|
||||
import world.phantasmal.web.externals.babylon.Scene
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.core.rendering.conversion.MeshBuilder
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToMeshBuilder
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToThree
|
||||
import world.phantasmal.web.core.rendering.disposeObject3DResources
|
||||
import world.phantasmal.web.externals.three.Group
|
||||
import world.phantasmal.web.externals.three.Object3D
|
||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||
import world.phantasmal.web.questEditor.models.SectionModel
|
||||
import world.phantasmal.web.questEditor.rendering.CollisionMetadata
|
||||
import world.phantasmal.web.questEditor.rendering.CollisionUserData
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
/**
|
||||
* Loads and caches area assets.
|
||||
*/
|
||||
class AreaAssetLoader(
|
||||
private val scope: CoroutineScope,
|
||||
scope: CoroutineScope,
|
||||
private val assetLoader: AssetLoader,
|
||||
private val scene: Scene,
|
||||
) : DisposableContainer() {
|
||||
/**
|
||||
* This cache's values consist of a TransformNode containing area render meshes and a list of
|
||||
* that area's sections.
|
||||
*/
|
||||
private val renderObjectCache = addDisposable(
|
||||
LoadingCache<CacheKey, Pair<TransformNode, List<SectionModel>>> { it.first.dispose() }
|
||||
LoadingCache<CacheKey, Pair<Object3D, List<SectionModel>>>(
|
||||
scope,
|
||||
{ (episode, areaVariant) ->
|
||||
val buffer = getAreaAsset(episode, areaVariant, AssetType.Render)
|
||||
val obj = parseAreaGeometry(buffer.cursor(Endianness.Little))
|
||||
areaGeometryToTransformNodeAndSections(obj, areaVariant)
|
||||
},
|
||||
{ (obj3d) -> disposeObject3DResources(obj3d) },
|
||||
)
|
||||
)
|
||||
|
||||
private val collisionObjectCache = addDisposable(
|
||||
LoadingCache<CacheKey, TransformNode> { it.dispose() }
|
||||
LoadingCache<CacheKey, Object3D>(
|
||||
scope,
|
||||
{ (episode, areaVariant) ->
|
||||
val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision)
|
||||
val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little))
|
||||
areaCollisionGeometryToTransformNode(obj, episode, areaVariant)
|
||||
},
|
||||
::disposeObject3DResources,
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun loadSections(episode: Episode, areaVariant: AreaVariantModel): List<SectionModel> =
|
||||
loadRenderGeometryAndSections(episode, areaVariant).second
|
||||
|
||||
suspend fun loadRenderGeometry(episode: Episode, areaVariant: AreaVariantModel): TransformNode =
|
||||
suspend fun loadRenderGeometry(episode: Episode, areaVariant: AreaVariantModel): Object3D =
|
||||
loadRenderGeometryAndSections(episode, areaVariant).first
|
||||
|
||||
private suspend fun loadRenderGeometryAndSections(
|
||||
episode: Episode,
|
||||
areaVariant: AreaVariantModel,
|
||||
): Pair<TransformNode, List<SectionModel>> =
|
||||
renderObjectCache.getOrPut(CacheKey(episode, areaVariant.area.id, areaVariant.id)) {
|
||||
scope.async {
|
||||
val buffer = getAreaAsset(episode, areaVariant, AssetType.Render)
|
||||
val obj = parseAreaGeometry(buffer.cursor(Endianness.Little))
|
||||
areaGeometryToTransformNodeAndSections(scene, obj, areaVariant)
|
||||
}
|
||||
}.await()
|
||||
): Pair<Object3D, List<SectionModel>> =
|
||||
renderObjectCache.get(CacheKey(episode, areaVariant))
|
||||
|
||||
suspend fun loadCollisionGeometry(
|
||||
episode: Episode,
|
||||
areaVariant: AreaVariantModel,
|
||||
): TransformNode =
|
||||
collisionObjectCache.getOrPut(CacheKey(episode, areaVariant.area.id, areaVariant.id)) {
|
||||
scope.async {
|
||||
val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision)
|
||||
val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little))
|
||||
areaCollisionGeometryToTransformNode(scene, obj, episode, areaVariant)
|
||||
}
|
||||
}.await()
|
||||
): Object3D =
|
||||
collisionObjectCache.get(CacheKey(episode, areaVariant))
|
||||
|
||||
private suspend fun getAreaAsset(
|
||||
episode: Episode,
|
||||
@ -87,8 +90,7 @@ class AreaAssetLoader(
|
||||
|
||||
private data class CacheKey(
|
||||
val episode: Episode,
|
||||
val areaId: Int,
|
||||
val areaVariantId: Int,
|
||||
val areaVariant: AreaVariantModel,
|
||||
)
|
||||
|
||||
private enum class AssetType {
|
||||
@ -96,9 +98,9 @@ class AreaAssetLoader(
|
||||
}
|
||||
}
|
||||
|
||||
class AreaMetadata(
|
||||
val section: SectionModel?,
|
||||
)
|
||||
interface AreaUserData {
|
||||
var sectionId: Int?
|
||||
}
|
||||
|
||||
private val AREA_BASE_NAMES: Map<Episode, List<Pair<String, Int>>> = mapOf(
|
||||
Episode.I to listOf(
|
||||
@ -185,57 +187,64 @@ private fun areaVersionToBaseUrl(episode: Episode, areaVariant: AreaVariantModel
|
||||
}
|
||||
|
||||
private fun areaGeometryToTransformNodeAndSections(
|
||||
scene: Scene,
|
||||
renderObject: RenderObject,
|
||||
areaVariant: AreaVariantModel,
|
||||
): Pair<TransformNode, List<SectionModel>> {
|
||||
): Pair<Object3D, List<SectionModel>> {
|
||||
val sections = mutableListOf<SectionModel>()
|
||||
val node = TransformNode("Render Geometry", scene)
|
||||
node.setEnabled(false)
|
||||
val obj3d = Group()
|
||||
|
||||
for (section in renderObject.sections) {
|
||||
val builder = VertexDataBuilder()
|
||||
val builder = MeshBuilder()
|
||||
|
||||
for (obj in section.objects) {
|
||||
ninjaObjectToVertexDataBuilder(obj, builder)
|
||||
ninjaObjectToMeshBuilder(obj, builder)
|
||||
}
|
||||
|
||||
val vertexData = builder.build()
|
||||
val mesh = Mesh("Render Geometry", scene, node)
|
||||
vertexData.applyToMesh(mesh)
|
||||
val mesh = builder.buildMesh()
|
||||
// TODO: Material.
|
||||
|
||||
mesh.position = vec3ToBabylon(section.position)
|
||||
mesh.rotation = vec3ToBabylon(section.rotation)
|
||||
mesh.position.set(
|
||||
section.position.x.toDouble(),
|
||||
section.position.y.toDouble(),
|
||||
section.position.z.toDouble()
|
||||
)
|
||||
mesh.rotation.set(
|
||||
section.rotation.x.toDouble(),
|
||||
section.rotation.y.toDouble(),
|
||||
section.rotation.z.toDouble(),
|
||||
)
|
||||
mesh.updateMatrixWorld()
|
||||
|
||||
if (section.id >= 0) {
|
||||
val sec = SectionModel(
|
||||
section.id,
|
||||
vec3ToBabylon(section.position),
|
||||
vec3ToBabylon(section.rotation),
|
||||
vec3ToThree(section.position),
|
||||
euler(section.rotation.x, section.rotation.y, section.rotation.z),
|
||||
areaVariant,
|
||||
)
|
||||
sections.add(sec)
|
||||
mesh.metadata = AreaMetadata(sec)
|
||||
}
|
||||
|
||||
(mesh.userData.unsafeCast<AreaUserData>()).sectionId = section.id.takeIf { it >= 0 }
|
||||
obj3d.add(mesh)
|
||||
}
|
||||
|
||||
return Pair(node, sections)
|
||||
return Pair(obj3d, sections)
|
||||
}
|
||||
|
||||
// TODO: Use Geometry and not BufferGeometry for better raycaster performance.
|
||||
private fun areaCollisionGeometryToTransformNode(
|
||||
scene: Scene,
|
||||
obj: CollisionObject,
|
||||
episode: Episode,
|
||||
areaVariant: AreaVariantModel,
|
||||
): TransformNode {
|
||||
val node = TransformNode(
|
||||
"Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}",
|
||||
scene
|
||||
)
|
||||
): Object3D {
|
||||
val obj3d = Group()
|
||||
obj3d.name = "Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}"
|
||||
|
||||
obj.meshes.forEachIndexed { i, collisionMesh ->
|
||||
val builder = VertexDataBuilder()
|
||||
for (collisionMesh in obj.meshes) {
|
||||
val builder = MeshBuilder()
|
||||
// TODO: Material.
|
||||
val group = builder.getGroupIndex(textureId = null, alpha = false, additiveBlending = false)
|
||||
|
||||
for (triangle in collisionMesh.triangles) {
|
||||
val isSectionTransition = (triangle.flags and 0b1000000) != 0
|
||||
@ -250,30 +259,26 @@ private fun areaCollisionGeometryToTransformNode(
|
||||
|
||||
// Filter out walls.
|
||||
if (colorIndex != 0) {
|
||||
val p1 = vec3ToBabylon(collisionMesh.vertices[triangle.index1])
|
||||
val p2 = vec3ToBabylon(collisionMesh.vertices[triangle.index2])
|
||||
val p3 = vec3ToBabylon(collisionMesh.vertices[triangle.index3])
|
||||
val n = vec3ToBabylon(triangle.normal)
|
||||
val p1 = vec3ToThree(collisionMesh.vertices[triangle.index1])
|
||||
val p2 = vec3ToThree(collisionMesh.vertices[triangle.index2])
|
||||
val p3 = vec3ToThree(collisionMesh.vertices[triangle.index3])
|
||||
val n = vec3ToThree(triangle.normal)
|
||||
|
||||
builder.addIndex(builder.vertexCount)
|
||||
builder.addIndex(group, builder.vertexCount)
|
||||
builder.addVertex(p1, n)
|
||||
builder.addIndex(builder.vertexCount)
|
||||
builder.addVertex(p3, n)
|
||||
builder.addIndex(builder.vertexCount)
|
||||
builder.addIndex(group, builder.vertexCount)
|
||||
builder.addVertex(p2, n)
|
||||
builder.addIndex(group, builder.vertexCount)
|
||||
builder.addVertex(p3, n)
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.vertexCount > 0) {
|
||||
val mesh = Mesh(
|
||||
"Collision Geometry $episode-${areaVariant.area.id}-${areaVariant.id}-$i",
|
||||
scene,
|
||||
parent = node
|
||||
)
|
||||
builder.build().applyToMesh(mesh)
|
||||
mesh.metadata = CollisionMetadata()
|
||||
val mesh = builder.buildMesh(boundingVolumes = true)
|
||||
(mesh.userData.unsafeCast<CollisionUserData>()).collisionMesh = true
|
||||
obj3d.add(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
return obj3d
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.loading
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import mu.KotlinLogging
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import world.phantasmal.core.PwResult
|
||||
@ -9,66 +8,43 @@ import world.phantasmal.core.Success
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseNj
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseXj
|
||||
import world.phantasmal.lib.fileFormats.ninja.*
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.lib.fileFormats.quest.ObjectType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToInstancedMesh
|
||||
import world.phantasmal.web.core.rendering.disposeObject3DResources
|
||||
import world.phantasmal.web.externals.three.CylinderBufferGeometry
|
||||
import world.phantasmal.web.externals.three.InstancedMesh
|
||||
import world.phantasmal.web.externals.three.MeshLambertMaterial
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.obj
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class EntityAssetLoader(
|
||||
private val scope: CoroutineScope,
|
||||
scope: CoroutineScope,
|
||||
private val assetLoader: AssetLoader,
|
||||
private val scene: Scene,
|
||||
) : DisposableContainer() {
|
||||
private val defaultMesh =
|
||||
MeshBuilder.CreateCylinder(
|
||||
"Entity",
|
||||
obj {
|
||||
diameter = 5.0
|
||||
height = 18.0
|
||||
},
|
||||
scene
|
||||
).apply {
|
||||
setEnabled(false)
|
||||
locallyTranslate(Vector3(0.0, 10.0, 0.0))
|
||||
bakeCurrentTransformIntoVertices()
|
||||
}
|
||||
|
||||
private val meshCache =
|
||||
addDisposable(LoadingCache<Pair<EntityType, Int?>, Mesh> { it.dispose() })
|
||||
|
||||
override fun internalDispose() {
|
||||
defaultMesh.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
suspend fun loadMesh(type: EntityType, model: Int?): Mesh =
|
||||
meshCache.getOrPut(Pair(type, model)) {
|
||||
scope.async {
|
||||
private val instancedMeshCache = addDisposable(
|
||||
LoadingCache<Pair<EntityType, Int?>, InstancedMesh>(
|
||||
scope,
|
||||
{ (type, model) ->
|
||||
try {
|
||||
loadGeometry(type, model)?.let { vertexData ->
|
||||
val mesh = Mesh("${type.uniqueName}${model?.let { "-$it" }}", scene)
|
||||
mesh.setEnabled(false)
|
||||
vertexData.applyToMesh(mesh)
|
||||
mesh
|
||||
} ?: defaultMesh
|
||||
loadMesh(type, model) ?: DEFAULT_MESH
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Couldn't load mesh for $type (model: $model)." }
|
||||
defaultMesh
|
||||
DEFAULT_MESH
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
},
|
||||
::disposeObject3DResources
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? {
|
||||
suspend fun loadInstancedMesh(type: EntityType, model: Int?): InstancedMesh =
|
||||
instancedMeshCache.get(Pair(type, model)).clone() as InstancedMesh
|
||||
|
||||
private suspend fun loadMesh(type: EntityType, model: Int?): InstancedMesh? {
|
||||
val geomFormat = entityTypeToGeometryFormat(type)
|
||||
|
||||
val geomParts = geometryParts(type).mapNotNull { suffix ->
|
||||
@ -78,9 +54,44 @@ class EntityAssetLoader(
|
||||
}
|
||||
}
|
||||
|
||||
return when (geomFormat) {
|
||||
val ninjaObject = when (geomFormat) {
|
||||
GeomFormat.Nj -> parseGeometry(type, geomParts, ::parseNj)
|
||||
GeomFormat.Xj -> parseGeometry(type, geomParts, ::parseXj)
|
||||
} ?: return null
|
||||
|
||||
val textures = loadTextures(type, model)
|
||||
|
||||
return ninjaObjectToInstancedMesh(
|
||||
ninjaObject,
|
||||
textures,
|
||||
maxInstances = 300,
|
||||
boundingVolumes = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadTextures(type: EntityType, model: Int?): List<XvrTexture> {
|
||||
val suffix =
|
||||
if (
|
||||
type === ObjectType.FloatingRocks ||
|
||||
(type === ObjectType.BigBrownRock && model == undefined)
|
||||
) {
|
||||
"-0"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// GeomFormat is irrelevant for textures.
|
||||
val path = entityTypeToPath(type, AssetType.Texture, suffix, model, GeomFormat.Nj)
|
||||
?: return emptyList()
|
||||
|
||||
val buffer = assetLoader.loadArrayBuffer(path)
|
||||
val xvm = parseXvm(buffer.cursor(endianness = Endianness.Little))
|
||||
|
||||
return if (xvm is Success) {
|
||||
xvm.value.textures
|
||||
} else {
|
||||
logger.warn { "Couldn't parse $path for $type." }
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,8 +99,8 @@ class EntityAssetLoader(
|
||||
type: EntityType,
|
||||
parts: List<Pair<String, ArrayBuffer>>,
|
||||
parse: (Cursor) -> PwResult<List<NinjaObject<Model>>>,
|
||||
): VertexData? {
|
||||
val njObjects = parts.flatMap { (path, data) ->
|
||||
): NinjaObject<Model>? {
|
||||
val ninjaObjects = parts.flatMap { (path, data) ->
|
||||
val njObjects = parse(data.cursor(Endianness.Little))
|
||||
|
||||
if (njObjects is Success && njObjects.value.isNotEmpty()) {
|
||||
@ -100,18 +111,30 @@ class EntityAssetLoader(
|
||||
}
|
||||
}
|
||||
|
||||
if (njObjects.isEmpty()) {
|
||||
if (ninjaObjects.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val njObject = njObjects.first()
|
||||
njObject.evaluationFlags.breakChildTrace = false
|
||||
val ninjaObject = ninjaObjects.first()
|
||||
ninjaObject.evaluationFlags.breakChildTrace = false
|
||||
|
||||
for (njObj in njObjects.drop(1)) {
|
||||
njObject.addChild(njObj)
|
||||
for (njObj in ninjaObjects.drop(1)) {
|
||||
ninjaObject.addChild(njObj)
|
||||
}
|
||||
|
||||
return ninjaObjectToVertexData(njObject)
|
||||
return ninjaObject
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_MESH = InstancedMesh(
|
||||
CylinderBufferGeometry(radiusTop = 2.5, radiusBottom = 2.5, height = 18.0).apply {
|
||||
translate(0.0, 10.0, 0.0)
|
||||
computeBoundingBox()
|
||||
computeBoundingSphere()
|
||||
},
|
||||
MeshLambertMaterial(),
|
||||
count = 1000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,20 @@
|
||||
package world.phantasmal.web.questEditor.loading
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
|
||||
class LoadingCache<K, V>(private val disposeValue: (V) -> Unit) : TrackedDisposable() {
|
||||
class LoadingCache<K, V>(
|
||||
private val scope: CoroutineScope,
|
||||
private val loadValue: suspend (K) -> V,
|
||||
private val disposeValue: (V) -> Unit,
|
||||
) : TrackedDisposable() {
|
||||
private val map = mutableMapOf<K, Deferred<V>>()
|
||||
|
||||
operator fun set(key: K, value: Deferred<V>) {
|
||||
map[key] = value
|
||||
}
|
||||
|
||||
@Suppress("DeferredIsResult")
|
||||
fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> =
|
||||
map.getOrPut(key, defaultValue)
|
||||
suspend fun get(key: K): V =
|
||||
map.getOrPut(key) { scope.async { loadValue(key) } }.await()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun internalDispose() {
|
||||
|
@ -1,26 +1,26 @@
|
||||
package world.phantasmal.web.questEditor.loading
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.Quest
|
||||
import world.phantasmal.lib.fileFormats.quest.parseQstToQuest
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
class QuestLoader(
|
||||
private val scope: CoroutineScope,
|
||||
scope: CoroutineScope,
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TrackedDisposable() {
|
||||
private val cache = LoadingCache<String, ArrayBuffer> {}
|
||||
|
||||
override fun internalDispose() {
|
||||
cache.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
) : DisposableContainer() {
|
||||
private val cache = addDisposable(
|
||||
LoadingCache<String, ArrayBuffer>(
|
||||
scope,
|
||||
{ path -> assetLoader.loadArrayBuffer("/quests$path") },
|
||||
{ /* Nothing to dispose. */ }
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun loadDefaultQuest(episode: Episode): Quest {
|
||||
require(episode == Episode.I) {
|
||||
@ -30,13 +30,6 @@ class QuestLoader(
|
||||
return loadQuest("/defaults/default_ep_1.qst")
|
||||
}
|
||||
|
||||
private suspend fun loadQuest(path: String): Quest {
|
||||
val buffer = cache.getOrPut(path) {
|
||||
scope.async {
|
||||
assetLoader.loadArrayBuffer("/quests$path")
|
||||
}
|
||||
}.await()
|
||||
|
||||
return parseQstToQuest(buffer.cursor(Endianness.Little)).unwrap().quest
|
||||
}
|
||||
private suspend fun loadQuest(path: String): Quest =
|
||||
parseQstToQuest(cache.get(path).cursor(Endianness.Little)).unwrap().quest
|
||||
}
|
||||
|
@ -14,4 +14,12 @@ class AreaModel(
|
||||
init {
|
||||
requireNonNegative(id, "id")
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class.js != other::class.js) return false
|
||||
return id == (other as AreaModel).id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = id
|
||||
}
|
||||
|
@ -16,4 +16,13 @@ class AreaVariantModel(val id: Int, val area: AreaModel) {
|
||||
fun setSections(sections: List<SectionModel>) {
|
||||
_sections.replaceAll(sections)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class.js != other::class.js) return false
|
||||
other as AreaVariantModel
|
||||
return id == other.id && area.id == other.area.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = 31 * id + area.hashCode()
|
||||
}
|
||||
|
@ -5,11 +5,14 @@ import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.lib.fileFormats.quest.QuestEntity
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.core.*
|
||||
import world.phantasmal.web.core.rendering.conversion.babylonToVec3
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon
|
||||
import world.phantasmal.web.externals.babylon.Quaternion
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.core.euler
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToThree
|
||||
import world.phantasmal.web.core.timesAssign
|
||||
import world.phantasmal.web.core.toEuler
|
||||
import world.phantasmal.web.externals.three.Euler
|
||||
import world.phantasmal.web.externals.three.Quaternion
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
import kotlin.math.PI
|
||||
|
||||
abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
@ -18,9 +21,9 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
private val _sectionId = mutableVal(entity.sectionId)
|
||||
private val _section = mutableVal<SectionModel?>(null)
|
||||
private val _sectionInitialized = mutableVal(false)
|
||||
private val _position = mutableVal(vec3ToBabylon(entity.position))
|
||||
private val _position = mutableVal(vec3ToThree(entity.position))
|
||||
private val _worldPosition = mutableVal(_position.value)
|
||||
private val _rotation = mutableVal(vec3ToBabylon(entity.rotation))
|
||||
private val _rotation = entity.rotation.let { mutableVal(euler(it.x, it.y, it.z)) }
|
||||
private val _worldRotation = mutableVal(_rotation.value)
|
||||
|
||||
val type: Type get() = entity.type
|
||||
@ -42,9 +45,9 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
/**
|
||||
* Section-relative rotation
|
||||
*/
|
||||
val rotation: Val<Vector3> = _rotation
|
||||
val rotation: Val<Euler> = _rotation
|
||||
|
||||
val worldRotation: Val<Vector3> = _worldRotation
|
||||
val worldRotation: Val<Euler> = _worldRotation
|
||||
|
||||
fun setSection(section: SectionModel) {
|
||||
require(section.areaVariant.area.id == areaId) {
|
||||
@ -67,37 +70,34 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
fun setPosition(pos: Vector3) {
|
||||
entity.position = babylonToVec3(pos)
|
||||
entity.setPosition(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
|
||||
|
||||
_position.value = pos
|
||||
|
||||
val section = section.value
|
||||
|
||||
_worldPosition.value =
|
||||
section?.rotationQuaternion?.transformed(pos)?.also {
|
||||
it += section.position
|
||||
} ?: pos
|
||||
if (section == null) pos
|
||||
else pos.clone().applyEuler(section.rotation).add(section.position)
|
||||
}
|
||||
|
||||
fun setWorldPosition(pos: Vector3) {
|
||||
_worldPosition.value = pos
|
||||
|
||||
val section = section.value
|
||||
|
||||
val relPos =
|
||||
if (section == null) pos
|
||||
else (pos - section.position).also {
|
||||
section.inverseRotationQuaternion.transform(it)
|
||||
}
|
||||
else (pos - section.position).applyEuler(section.inverseRotation)
|
||||
|
||||
entity.position = babylonToVec3(relPos)
|
||||
entity.setPosition(relPos.x.toFloat(), relPos.y.toFloat(), relPos.z.toFloat())
|
||||
|
||||
_worldPosition.value = pos
|
||||
_position.value = relPos
|
||||
}
|
||||
|
||||
fun setRotation(rot: Vector3) {
|
||||
fun setRotation(rot: Euler) {
|
||||
floorModEuler(rot)
|
||||
|
||||
entity.rotation = babylonToVec3(rot)
|
||||
entity.setRotation(rot.x.toFloat(), rot.y.toFloat(), rot.z.toFloat())
|
||||
_rotation.value = rot
|
||||
|
||||
val section = section.value
|
||||
@ -105,55 +105,39 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
if (section == null) {
|
||||
_worldRotation.value = rot
|
||||
} else {
|
||||
Quaternion.FromEulerAnglesToRef(rot.x, rot.y, rot.z, q1)
|
||||
Quaternion.FromEulerAnglesToRef(
|
||||
section.rotation.x,
|
||||
section.rotation.y,
|
||||
section.rotation.z,
|
||||
q2
|
||||
)
|
||||
q1.setFromEuler(rot)
|
||||
q2.setFromEuler(section.rotation)
|
||||
q1 *= q2
|
||||
val worldRot = q1.toEulerAngles()
|
||||
floorModEuler(worldRot)
|
||||
_worldRotation.value = worldRot
|
||||
_worldRotation.value = floorModEuler(q1.toEuler())
|
||||
}
|
||||
}
|
||||
|
||||
fun setWorldRotation(rot: Vector3) {
|
||||
fun setWorldRotation(rot: Euler) {
|
||||
floorModEuler(rot)
|
||||
|
||||
_worldRotation.value = rot
|
||||
|
||||
val section = section.value
|
||||
|
||||
val relRot = if (section == null) {
|
||||
rot
|
||||
} else {
|
||||
Quaternion.FromEulerAnglesToRef(rot.x, rot.y, rot.z, q1)
|
||||
Quaternion.FromEulerAnglesToRef(
|
||||
section.rotation.x,
|
||||
section.rotation.y,
|
||||
section.rotation.z,
|
||||
q2
|
||||
)
|
||||
q2.invert()
|
||||
q1.setFromEuler(rot)
|
||||
q2.setFromEuler(section.rotation)
|
||||
q2.inverse()
|
||||
q1 *= q2
|
||||
val relRot = q1.toEulerAngles()
|
||||
floorModEuler(relRot)
|
||||
relRot
|
||||
floorModEuler(q1.toEuler())
|
||||
}
|
||||
|
||||
entity.rotation = babylonToVec3(relRot)
|
||||
entity.setRotation(relRot.x.toFloat(), relRot.y.toFloat(), relRot.z.toFloat())
|
||||
_worldRotation.value = rot
|
||||
_rotation.value = relRot
|
||||
}
|
||||
|
||||
private fun floorModEuler(euler: Vector3) {
|
||||
private fun floorModEuler(euler: Euler): Euler =
|
||||
euler.set(
|
||||
floorMod(euler.x, 2 * PI),
|
||||
floorMod(euler.y, 2 * PI),
|
||||
floorMod(euler.z, 2 * PI),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// These quaternions are used as temporary variables to avoid memory allocation.
|
||||
|
@ -1,13 +1,14 @@
|
||||
package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.web.core.inverse
|
||||
import world.phantasmal.web.externals.babylon.Quaternion
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.core.toEuler
|
||||
import world.phantasmal.web.core.toQuaternion
|
||||
import world.phantasmal.web.externals.three.Euler
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
|
||||
class SectionModel(
|
||||
val id: Int,
|
||||
val position: Vector3,
|
||||
val rotation: Vector3,
|
||||
val rotation: Euler,
|
||||
val areaVariant: AreaVariantModel,
|
||||
) {
|
||||
init {
|
||||
@ -16,8 +17,5 @@ class SectionModel(
|
||||
}
|
||||
}
|
||||
|
||||
val rotationQuaternion: Quaternion =
|
||||
Quaternion.FromEulerAngles(rotation.x, rotation.y, rotation.z)
|
||||
|
||||
val inverseRotationQuaternion: Quaternion = rotationQuaternion.inverse()
|
||||
val inverseRotation: Euler = rotation.toQuaternion().inverse().toEuler()
|
||||
}
|
||||
|
@ -12,19 +12,14 @@ class AreaMeshManager(
|
||||
private val areaAssetLoader: AreaAssetLoader,
|
||||
) {
|
||||
suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) {
|
||||
renderer.collisionGeometry?.setEnabled(false)
|
||||
renderer.collisionGeometry = null
|
||||
|
||||
if (episode == null || areaVariant == null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
||||
// Call setEnabled(false) on renderer.collisionGeometry before calling setEnabled(true)
|
||||
// on geom, because they can refer to the same object.
|
||||
renderer.collisionGeometry?.setEnabled(false)
|
||||
geom.setEnabled(true)
|
||||
renderer.collisionGeometry = geom
|
||||
renderer.collisionGeometry = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) {
|
||||
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
|
||||
|
@ -3,10 +3,14 @@ package world.phantasmal.web.questEditor.rendering
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.externals.babylon.AbstractMesh
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.externals.three.Group
|
||||
import world.phantasmal.web.externals.three.InstancedMesh
|
||||
import world.phantasmal.web.externals.three.Mesh
|
||||
import world.phantasmal.web.externals.three.Object3D
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.LoadingCache
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
||||
@ -19,58 +23,74 @@ private val logger = KotlinLogging.logger {}
|
||||
class EntityMeshManager(
|
||||
private val scope: CoroutineScope,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
renderer: QuestRenderer,
|
||||
private val renderer: QuestRenderer,
|
||||
private val entityAssetLoader: EntityAssetLoader,
|
||||
) : DisposableContainer() {
|
||||
private val entityMeshes = Group().apply { name = "Entities" }
|
||||
|
||||
private val meshCache = addDisposable(
|
||||
LoadingCache<CacheKey, InstancedMesh>(
|
||||
scope,
|
||||
{ (type, model) ->
|
||||
val mesh = entityAssetLoader.loadInstancedMesh(type, model)
|
||||
entityMeshes.add(mesh)
|
||||
mesh
|
||||
},
|
||||
{ /* Nothing to dispose. */ },
|
||||
)
|
||||
)
|
||||
|
||||
private val queue: MutableList<QuestEntityModel<*, *>> = mutableListOf()
|
||||
private val loadedEntities: MutableMap<QuestEntityModel<*, *>, LoadedEntity> = mutableMapOf()
|
||||
private val loadedEntities: MutableList<LoadedEntity> = mutableListOf()
|
||||
private var loading = false
|
||||
|
||||
private var entityMeshes = TransformNode("Entities", renderer.scene)
|
||||
private var hoveredMesh: AbstractMesh? = null
|
||||
private var selectedMesh: AbstractMesh? = null
|
||||
private var hoveredMesh: Mesh? = null
|
||||
private var selectedMesh: Mesh? = null
|
||||
|
||||
init {
|
||||
observe(questEditorStore.selectedEntity) { entity ->
|
||||
if (entity == null) {
|
||||
unmarkSelected()
|
||||
} else {
|
||||
val loaded = loadedEntities[entity]
|
||||
renderer.scene.add(entityMeshes)
|
||||
|
||||
// Mesh might not be loaded yet.
|
||||
if (loaded == null) {
|
||||
unmarkSelected()
|
||||
} else {
|
||||
markSelected(loaded.mesh)
|
||||
}
|
||||
}
|
||||
}
|
||||
// observe(questEditorStore.selectedEntity) { entity ->
|
||||
// if (entity == null) {
|
||||
// unmarkSelected()
|
||||
// } else {
|
||||
// val loaded = loadedEntities[entity]
|
||||
//
|
||||
// // Mesh might not be loaded yet.
|
||||
// if (loaded == null) {
|
||||
// unmarkSelected()
|
||||
// } else {
|
||||
// markSelected(loaded.mesh)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
entityMeshes.dispose()
|
||||
renderer.scene.remove(entityMeshes)
|
||||
removeAll()
|
||||
entityMeshes.clear()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
fun add(entities: List<QuestEntityModel<*, *>>) {
|
||||
queue.addAll(entities)
|
||||
fun add(entity: QuestEntityModel<*, *>) {
|
||||
queue.add(entity)
|
||||
|
||||
if (!loading) {
|
||||
loading = true
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
loading = true
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val entity = queue.first()
|
||||
val queuedEntity = queue.first()
|
||||
|
||||
try {
|
||||
load(entity)
|
||||
load(queuedEntity)
|
||||
} catch (e: Error) {
|
||||
logger.error(e) {
|
||||
"Couldn't load model for entity of type ${entity.type}."
|
||||
"Couldn't load model for entity of type ${queuedEntity.type}."
|
||||
}
|
||||
queue.remove(entity)
|
||||
queue.remove(queuedEntity)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@ -80,16 +100,27 @@ class EntityMeshManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(entities: List<QuestEntityModel<*, *>>) {
|
||||
for (entity in entities) {
|
||||
queue.remove(entity)
|
||||
fun remove(entity: QuestEntityModel<*, *>) {
|
||||
queue.remove(entity)
|
||||
|
||||
loadedEntities.remove(entity)?.dispose()
|
||||
val idx = loadedEntities.indexOfFirst { it.entity == entity }
|
||||
|
||||
if (idx != -1) {
|
||||
val loaded = loadedEntities.removeAt(idx)
|
||||
loaded.mesh.count--
|
||||
|
||||
for (i in idx until loaded.mesh.count) {
|
||||
loaded.mesh.instanceMatrix.copyAt(i, loaded.mesh.instanceMatrix, i + 1)
|
||||
loadedEntities[i].instanceIndex = i
|
||||
}
|
||||
|
||||
loaded.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
for (loaded in loadedEntities.values) {
|
||||
for (loaded in loadedEntities) {
|
||||
loaded.mesh.count = 0
|
||||
loaded.dispose()
|
||||
}
|
||||
|
||||
@ -97,59 +128,64 @@ class EntityMeshManager(
|
||||
queue.clear()
|
||||
}
|
||||
|
||||
private fun markSelected(entityMesh: AbstractMesh) {
|
||||
if (entityMesh == hoveredMesh) {
|
||||
hoveredMesh = null
|
||||
}
|
||||
|
||||
if (entityMesh != selectedMesh) {
|
||||
selectedMesh?.let { it.showBoundingBox = false }
|
||||
|
||||
entityMesh.showBoundingBox = true
|
||||
}
|
||||
|
||||
selectedMesh = entityMesh
|
||||
}
|
||||
|
||||
private fun unmarkSelected() {
|
||||
selectedMesh?.let { it.showBoundingBox = false }
|
||||
selectedMesh = null
|
||||
}
|
||||
// private fun markSelected(entityMesh: AbstractMesh) {
|
||||
// if (entityMesh == hoveredMesh) {
|
||||
// hoveredMesh = null
|
||||
// }
|
||||
//
|
||||
// if (entityMesh != selectedMesh) {
|
||||
// selectedMesh?.let { it.showBoundingBox = false }
|
||||
//
|
||||
// entityMesh.showBoundingBox = true
|
||||
// }
|
||||
//
|
||||
// selectedMesh = entityMesh
|
||||
// }
|
||||
//
|
||||
// private fun unmarkSelected() {
|
||||
// selectedMesh?.let { it.showBoundingBox = false }
|
||||
// selectedMesh = null
|
||||
// }
|
||||
|
||||
private suspend fun load(entity: QuestEntityModel<*, *>) {
|
||||
val mesh = entityAssetLoader.loadMesh(
|
||||
val mesh = meshCache.get(CacheKey(
|
||||
type = entity.type,
|
||||
model = (entity as? QuestObjectModel)?.model?.value
|
||||
)
|
||||
))
|
||||
|
||||
// Only add an instance of this mesh if the entity is still in the queue at this point.
|
||||
if (queue.remove(entity)) {
|
||||
val instance = mesh.createInstance(entity.type.uniqueName)
|
||||
instance.parent = entityMeshes
|
||||
val instanceIndex = mesh.count
|
||||
mesh.count++
|
||||
|
||||
if (entity == questEditorStore.selectedEntity.value) {
|
||||
markSelected(instance)
|
||||
}
|
||||
// if (entity == questEditorStore.selectedEntity.value) {
|
||||
// markSelected(instance)
|
||||
// }
|
||||
|
||||
loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave)
|
||||
loadedEntities.add(LoadedEntity(
|
||||
entity,
|
||||
mesh,
|
||||
instanceIndex,
|
||||
questEditorStore.selectedWave
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private data class CacheKey(val type: EntityType, val model: Int?)
|
||||
|
||||
private inner class LoadedEntity(
|
||||
entity: QuestEntityModel<*, *>,
|
||||
val mesh: AbstractMesh,
|
||||
val entity: QuestEntityModel<*, *>,
|
||||
val mesh: InstancedMesh,
|
||||
var instanceIndex: Int,
|
||||
selectedWave: Val<WaveModel?>,
|
||||
) : DisposableContainer() {
|
||||
init {
|
||||
mesh.metadata = EntityMetadata(entity)
|
||||
updateMatrix()
|
||||
|
||||
observe(entity.worldPosition) { pos ->
|
||||
mesh.position = pos
|
||||
}
|
||||
|
||||
observe(entity.worldRotation) { rot ->
|
||||
mesh.rotation = rot
|
||||
}
|
||||
addDisposables(
|
||||
entity.worldPosition.observe { updateMatrix() },
|
||||
entity.worldRotation.observe { updateMatrix() },
|
||||
)
|
||||
|
||||
val isVisible: Val<Boolean>
|
||||
|
||||
@ -166,21 +202,40 @@ class EntityMeshManager(
|
||||
|
||||
if (entity is QuestObjectModel) {
|
||||
addDisposable(entity.model.observe(callNow = false) {
|
||||
remove(listOf(entity))
|
||||
add(listOf(entity))
|
||||
remove(entity)
|
||||
add(entity)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
observe(isVisible) { visible ->
|
||||
mesh.setEnabled(visible)
|
||||
}
|
||||
// observe(isVisible) { visible ->
|
||||
// mesh.setEnabled(visible)
|
||||
// }
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
mesh.parent = null
|
||||
mesh.dispose()
|
||||
// TODO: Dispose instance.
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
private fun updateMatrix() {
|
||||
instanceHelper.position.set(
|
||||
entity.worldPosition.value.x,
|
||||
entity.worldPosition.value.y,
|
||||
entity.worldPosition.value.z,
|
||||
)
|
||||
instanceHelper.rotation.set(
|
||||
entity.worldRotation.value.x,
|
||||
entity.worldRotation.value.y,
|
||||
entity.worldRotation.value.z,
|
||||
)
|
||||
instanceHelper.updateMatrix()
|
||||
mesh.setMatrixAt(instanceIndex, instanceHelper.matrix)
|
||||
mesh.instanceMatrix.needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val instanceHelper = Object3D()
|
||||
}
|
||||
}
|
||||
|
@ -4,4 +4,6 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
|
||||
class EntityMetadata(val entity: QuestEntityModel<*, *>)
|
||||
|
||||
class CollisionMetadata
|
||||
interface CollisionUserData {
|
||||
var collisionMesh: Boolean
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class QuestEditorMeshManager(
|
||||
entityAssetLoader: EntityAssetLoader,
|
||||
) : QuestMeshManager(scope, questEditorStore, renderer, areaAssetLoader, entityAssetLoader) {
|
||||
init {
|
||||
disposer.addAll(
|
||||
addDisposables(
|
||||
questEditorStore.currentQuest.map(questEditorStore.currentArea, ::getAreaVariantDetails)
|
||||
.observe { (details) ->
|
||||
loadMeshes(details.episode, details.areaVariant, details.npcs, details.objects)
|
||||
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.ListValChangeEvent
|
||||
@ -14,6 +13,7 @@ import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
/**
|
||||
* Loads the necessary area and entity 3D models into [QuestRenderer].
|
||||
@ -24,15 +24,13 @@ abstract class QuestMeshManager protected constructor(
|
||||
private val renderer: QuestRenderer,
|
||||
areaAssetLoader: AreaAssetLoader,
|
||||
entityAssetLoader: EntityAssetLoader,
|
||||
) : TrackedDisposable() {
|
||||
protected val disposer = Disposer()
|
||||
|
||||
private val areaDisposer = disposer.add(Disposer())
|
||||
) : DisposableContainer() {
|
||||
private val areaDisposer = addDisposable(Disposer())
|
||||
private val areaMeshManager = AreaMeshManager(renderer, areaAssetLoader)
|
||||
private val npcMeshManager = disposer.add(
|
||||
private val npcMeshManager = addDisposable(
|
||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||
)
|
||||
private val objectMeshManager = disposer.add(
|
||||
private val objectMeshManager = addDisposable(
|
||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||
)
|
||||
|
||||
@ -64,22 +62,17 @@ abstract class QuestMeshManager protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
disposer.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
private fun npcsChanged(change: ListValChangeEvent<QuestNpcModel>) {
|
||||
if (change is ListValChangeEvent.Change) {
|
||||
npcMeshManager.remove(change.removed)
|
||||
npcMeshManager.add(change.inserted)
|
||||
change.removed.forEach(npcMeshManager::remove)
|
||||
change.inserted.forEach(npcMeshManager::add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun objectsChanged(change: ListValChangeEvent<QuestObjectModel>) {
|
||||
if (change is ListValChangeEvent.Change) {
|
||||
objectMeshManager.remove(change.removed)
|
||||
objectMeshManager.add(change.inserted)
|
||||
change.removed.forEach(objectMeshManager::remove)
|
||||
change.inserted.forEach(objectMeshManager::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +1,45 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.max
|
||||
import world.phantasmal.web.externals.three.Object3D
|
||||
import world.phantasmal.web.externals.three.PerspectiveCamera
|
||||
|
||||
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
||||
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||
|
||||
var collisionGeometry: TransformNode? = null
|
||||
class QuestRenderer(
|
||||
createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : Renderer(
|
||||
createThreeRenderer,
|
||||
PerspectiveCamera(
|
||||
fov = 45.0,
|
||||
aspect = 1.0,
|
||||
near = 10.0,
|
||||
far = 5_000.0
|
||||
)
|
||||
) {
|
||||
var collisionGeometry: Object3D? = null
|
||||
set(geom) {
|
||||
field?.let { scene.remove(it) }
|
||||
field = geom
|
||||
geom?.let { scene.add(it) }
|
||||
}
|
||||
|
||||
init {
|
||||
with(camera) {
|
||||
inertia = 0.0
|
||||
angularSensibilityX = 200.0
|
||||
angularSensibilityY = 200.0
|
||||
// Set lowerBetaLimit to avoid shitty camera implementation from breaking completely
|
||||
// when looking directly down.
|
||||
lowerBetaLimit = 0.4
|
||||
panningInertia = 0.0
|
||||
panningAxis = Vector3(1.0, 0.0, -1.0)
|
||||
pinchDeltaPercentage = 0.1
|
||||
wheelDeltaPercentage = 0.1
|
||||
camera.position.set(0.0, 50.0, 200.0)
|
||||
controls.update()
|
||||
|
||||
updatePanningSensibility()
|
||||
onViewMatrixChangedObservable.add({ _, _ ->
|
||||
updatePanningSensibility()
|
||||
})
|
||||
|
||||
enableCameraControls()
|
||||
|
||||
camera.storeState()
|
||||
}
|
||||
controls.screenSpacePanning = false
|
||||
}
|
||||
|
||||
fun resetCamera() {
|
||||
camera.restoreState()
|
||||
}
|
||||
|
||||
fun enableCameraControls() {
|
||||
camera.attachControl(
|
||||
canvas,
|
||||
noPreventDefault = false,
|
||||
useCtrlForPanning = false,
|
||||
panningMouseButton = 0
|
||||
)
|
||||
}
|
||||
|
||||
fun disableCameraControls() {
|
||||
camera.detachControl()
|
||||
}
|
||||
|
||||
override fun render() {
|
||||
camera.minZ = max(0.01, camera.radius / 100)
|
||||
camera.maxZ = max(2_000.0, 10 * camera.radius)
|
||||
super.render()
|
||||
}
|
||||
|
||||
/**
|
||||
* Make "panningSensibility" an inverse function of radius to make panning work "sensibly" at
|
||||
* all distances.
|
||||
*/
|
||||
private fun updatePanningSensibility() {
|
||||
camera.panningSensibility = 1_000 / camera.radius
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import kotlinx.browser.document
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.core.plusAssign
|
||||
import world.phantasmal.web.core.times
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.web.externals.three.Intersection
|
||||
import world.phantasmal.web.externals.three.Raycaster
|
||||
import world.phantasmal.web.externals.three.Vector2
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
import world.phantasmal.web.questEditor.actions.TranslateEntityAction
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.SectionModel
|
||||
@ -17,16 +17,18 @@ import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val ZERO_VECTOR = Vector3.Zero()
|
||||
private val DOWN_VECTOR = Vector3.Down()
|
||||
private val ZERO_VECTOR = Vector3(0.0, 0.0, 0.0)
|
||||
private val DOWN_VECTOR = Vector3(0.0, -1.0, 0.0)
|
||||
|
||||
private val raycaster = Raycaster()
|
||||
|
||||
class UserInputManager(
|
||||
questEditorStore: QuestEditorStore,
|
||||
private val renderer: QuestRenderer,
|
||||
) : DisposableContainer() {
|
||||
private val stateContext = StateContext(questEditorStore, renderer)
|
||||
private val pointerPosition = Vector2.Zero()
|
||||
private val lastPointerPosition = Vector2.Zero()
|
||||
private val pointerPosition = Vector2()
|
||||
private val lastPointerPosition = Vector2()
|
||||
private var movedSinceLastPointerDown = false
|
||||
private var state: State
|
||||
private var onPointerUpListener: Disposable? = null
|
||||
@ -128,7 +130,7 @@ class UserInputManager(
|
||||
}
|
||||
}
|
||||
|
||||
lastPointerPosition.copyFrom(pointerPosition)
|
||||
lastPointerPosition.copy(pointerPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,8 +138,8 @@ private class StateContext(
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
val renderer: QuestRenderer,
|
||||
) {
|
||||
private val plane = Plane.FromPositionAndNormal(Vector3.Up(), Vector3.Up())
|
||||
private val ray = Ray.Zero()
|
||||
// private val plane = Plane.FromPositionAndNormal(Vector3.Up(), Vector3.Up())
|
||||
// private val ray = Ray.Zero()
|
||||
|
||||
val scene = renderer.scene
|
||||
|
||||
@ -154,7 +156,7 @@ private class StateContext(
|
||||
if (vertically) {
|
||||
// TODO: Vertical translation.
|
||||
} else {
|
||||
translateEntityHorizontally(entity, dragAdjust, grabOffset)
|
||||
// translateEntityHorizontally(entity, dragAdjust, grabOffset)
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,79 +183,79 @@ private class StateContext(
|
||||
* If the drag-adjusted pointer is over the ground, translate an entity horizontally across the
|
||||
* ground. Otherwise translate the entity over the horizontal plane that intersects its origin.
|
||||
*/
|
||||
private fun translateEntityHorizontally(
|
||||
entity: QuestEntityModel<*, *>,
|
||||
dragAdjust: Vector3,
|
||||
grabOffset: Vector3,
|
||||
) {
|
||||
val pick = pickGround(scene.pointerX, scene.pointerY, dragAdjust)
|
||||
|
||||
if (pick == null) {
|
||||
// If the pointer is not over the ground, we translate the entity across the horizontal
|
||||
// plane in which the entity's origin lies.
|
||||
scene.createPickingRayToRef(
|
||||
scene.pointerX,
|
||||
scene.pointerY,
|
||||
Matrix.IdentityReadOnly,
|
||||
ray,
|
||||
renderer.camera
|
||||
)
|
||||
|
||||
plane.d = -entity.worldPosition.value.y + grabOffset.y
|
||||
|
||||
ray.intersectsPlane(plane)?.let { distance ->
|
||||
// Compute the intersection point.
|
||||
val pos = ray.direction * distance
|
||||
pos += ray.origin
|
||||
// Compute the entity's new world position.
|
||||
pos.x += grabOffset.x
|
||||
pos.y = entity.worldPosition.value.y
|
||||
pos.z += grabOffset.z
|
||||
|
||||
entity.setWorldPosition(pos)
|
||||
}
|
||||
} else {
|
||||
// TODO: Set entity section.
|
||||
entity.setWorldPosition(
|
||||
Vector3(
|
||||
pick.pickedPoint!!.x,
|
||||
pick.pickedPoint.y + grabOffset.y - dragAdjust.y,
|
||||
pick.pickedPoint.z,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun pickGround(x: Double, y: Double, dragAdjust: Vector3 = ZERO_VECTOR): PickingInfo? {
|
||||
scene.createPickingRayToRef(
|
||||
x,
|
||||
y,
|
||||
Matrix.IdentityReadOnly,
|
||||
ray,
|
||||
renderer.camera
|
||||
)
|
||||
|
||||
ray.origin += dragAdjust
|
||||
|
||||
val pickingInfoArray = scene.multiPickWithRay(
|
||||
ray,
|
||||
{ it.isEnabled() && it.metadata is CollisionMetadata },
|
||||
)
|
||||
|
||||
if (pickingInfoArray != null) {
|
||||
for (pickingInfo in pickingInfoArray) {
|
||||
pickingInfo.getNormal()?.let { n ->
|
||||
// Don't allow entities to be placed on very steep terrain. E.g. walls.
|
||||
// TODO: make use of the flags field in the collision data.
|
||||
if (n.y > 0.75) {
|
||||
return pickingInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
// private fun translateEntityHorizontally(
|
||||
// entity: QuestEntityModel<*, *>,
|
||||
// dragAdjust: Vector3,
|
||||
// grabOffset: Vector3,
|
||||
// ) {
|
||||
// val pick = pickGround(scene.pointerX, scene.pointerY, dragAdjust)
|
||||
//
|
||||
// if (pick == null) {
|
||||
// // If the pointer is not over the ground, we translate the entity across the horizontal
|
||||
// // plane in which the entity's origin lies.
|
||||
// scene.createPickingRayToRef(
|
||||
// scene.pointerX,
|
||||
// scene.pointerY,
|
||||
// Matrix.IdentityReadOnly,
|
||||
// ray,
|
||||
// renderer.camera
|
||||
// )
|
||||
//
|
||||
// plane.d = -entity.worldPosition.value.y + grabOffset.y
|
||||
//
|
||||
// ray.intersectsPlane(plane)?.let { distance ->
|
||||
// // Compute the intersection point.
|
||||
// val pos = ray.direction * distance
|
||||
// pos += ray.origin
|
||||
// // Compute the entity's new world position.
|
||||
// pos.x += grabOffset.x
|
||||
// pos.y = entity.worldPosition.value.y
|
||||
// pos.z += grabOffset.z
|
||||
//
|
||||
// entity.setWorldPosition(pos)
|
||||
// }
|
||||
// } else {
|
||||
// // TODO: Set entity section.
|
||||
// entity.setWorldPosition(
|
||||
// Vector3(
|
||||
// pick.pickedPoint!!.x,
|
||||
// pick.pickedPoint.y + grabOffset.y - dragAdjust.y,
|
||||
// pick.pickedPoint.z,
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun pickGround(x: Double, y: Double, dragAdjust: Vector3 = ZERO_VECTOR): PickingInfo? {
|
||||
// scene.createPickingRayToRef(
|
||||
// x,
|
||||
// y,
|
||||
// Matrix.IdentityReadOnly,
|
||||
// ray,
|
||||
// renderer.camera
|
||||
// )
|
||||
//
|
||||
// ray.origin += dragAdjust
|
||||
//
|
||||
// val pickingInfoArray = scene.multiPickWithRay(
|
||||
// ray,
|
||||
// { it.isEnabled() && it.metadata is CollisionUserData },
|
||||
// )
|
||||
//
|
||||
// if (pickingInfoArray != null) {
|
||||
// for (pickingInfo in pickingInfoArray) {
|
||||
// pickingInfo.getNormal()?.let { n ->
|
||||
// // Don't allow entities to be placed on very steep terrain. E.g. walls.
|
||||
// // TODO: make use of the flags field in the collision data.
|
||||
// if (n.y > 0.75) {
|
||||
// return pickingInfo
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return null
|
||||
// }
|
||||
}
|
||||
|
||||
private sealed class Evt
|
||||
@ -284,7 +286,7 @@ private class PointerMoveEvt(
|
||||
|
||||
private class Pick(
|
||||
val entity: QuestEntityModel<*, *>,
|
||||
val mesh: AbstractMesh,
|
||||
// val mesh: AbstractMesh,
|
||||
|
||||
/**
|
||||
* Vector that points from the grabbing point (somewhere on the model's surface) to the entity's
|
||||
@ -319,40 +321,40 @@ private class IdleState(
|
||||
) : State() {
|
||||
override fun processEvent(event: Evt): State {
|
||||
when (event) {
|
||||
is PointerDownEvt -> {
|
||||
pickEntity()?.let { pick ->
|
||||
when (event.buttons) {
|
||||
1 -> {
|
||||
ctx.setSelectedEntity(pick.entity)
|
||||
// is PointerDownEvt -> {
|
||||
// pickEntity()?.let { pick ->
|
||||
// when (event.buttons) {
|
||||
// 1 -> {
|
||||
// ctx.setSelectedEntity(pick.entity)
|
||||
//
|
||||
// if (entityManipulationEnabled) {
|
||||
// return TranslationState(
|
||||
// ctx,
|
||||
// pick.entity,
|
||||
// pick.dragAdjust,
|
||||
// pick.grabOffset
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// 2 -> {
|
||||
// ctx.setSelectedEntity(pick.entity)
|
||||
//
|
||||
// if (entityManipulationEnabled) {
|
||||
// // TODO: Enter RotationState.
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (entityManipulationEnabled) {
|
||||
return TranslationState(
|
||||
ctx,
|
||||
pick.entity,
|
||||
pick.dragAdjust,
|
||||
pick.grabOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
ctx.setSelectedEntity(pick.entity)
|
||||
|
||||
if (entityManipulationEnabled) {
|
||||
// TODO: Enter RotationState.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PointerUpEvt -> {
|
||||
updateCameraTarget()
|
||||
|
||||
// If the user clicks on nothing, deselect the currently selected entity.
|
||||
if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
||||
ctx.setSelectedEntity(null)
|
||||
}
|
||||
}
|
||||
// is PointerUpEvt -> {
|
||||
// updateCameraTarget()
|
||||
//
|
||||
// // If the user clicks on nothing, deselect the currently selected entity.
|
||||
// if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
||||
// ctx.setSelectedEntity(null)
|
||||
// }
|
||||
// }
|
||||
|
||||
else -> {
|
||||
// Do nothing.
|
||||
@ -369,44 +371,48 @@ private class IdleState(
|
||||
private fun updateCameraTarget() {
|
||||
// If the user moved the camera, try setting the camera
|
||||
// target to a better point.
|
||||
ctx.pickGround(
|
||||
ctx.renderer.engine.getRenderWidth() / 2,
|
||||
ctx.renderer.engine.getRenderHeight() / 2,
|
||||
)?.pickedPoint?.let { newTarget ->
|
||||
ctx.renderer.camera.target = newTarget
|
||||
}
|
||||
// ctx.pickGround(
|
||||
// ctx.renderer.engine.getRenderWidth() / 2,
|
||||
// ctx.renderer.engine.getRenderHeight() / 2,
|
||||
// )?.pickedPoint?.let { newTarget ->
|
||||
// ctx.renderer.camera.target = newTarget
|
||||
// }
|
||||
}
|
||||
|
||||
private fun pickEntity(): Pick? {
|
||||
// Find the nearest object and NPC under the pointer.
|
||||
val pickInfo = ctx.scene.pick(ctx.scene.pointerX, ctx.scene.pointerY)
|
||||
if (pickInfo?.pickedMesh == null) return null
|
||||
|
||||
val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
||||
?: return null
|
||||
|
||||
// Vector from the point where we grab the entity to its position.
|
||||
val grabOffset = pickInfo.pickedMesh.position - pickInfo.pickedPoint!!
|
||||
|
||||
// Vector from the point where we grab the entity to the point on the ground right beneath
|
||||
// its position. The same as grabOffset when an entity is standing on the ground.
|
||||
val dragAdjust = grabOffset.clone()
|
||||
|
||||
// Find vertical distance to the ground.
|
||||
ctx.scene.pickWithRay(
|
||||
Ray(pickInfo.pickedMesh.position, DOWN_VECTOR),
|
||||
{ it.isEnabled() && it.metadata is CollisionMetadata },
|
||||
)?.let { groundPick ->
|
||||
dragAdjust.y -= groundPick.distance
|
||||
}
|
||||
|
||||
return Pick(
|
||||
entity,
|
||||
pickInfo.pickedMesh,
|
||||
grabOffset,
|
||||
dragAdjust,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @param pointerPosition pointer coordinates in normalized device space
|
||||
*/
|
||||
// private fun pickEntity(pointerPosition:Vector2): Pick? {
|
||||
// // Find the nearest object and NPC under the pointer.
|
||||
// raycaster.setFromCamera(pointerPosition, ctx.renderer.camera)
|
||||
// val pickInfo = ctx.scene.pick(ctx.scene.pointerX, ctx.scene.pointerY)
|
||||
// if (pickInfo?.pickedMesh == null) return null
|
||||
//
|
||||
// val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
||||
// ?: return null
|
||||
//
|
||||
// // Vector from the point where we grab the entity to its position.
|
||||
// val grabOffset = pickInfo.pickedMesh.position - pickInfo.pickedPoint!!
|
||||
//
|
||||
// // Vector from the point where we grab the entity to the point on the ground right beneath
|
||||
// // its position. The same as grabOffset when an entity is standing on the ground.
|
||||
// val dragAdjust = grabOffset.clone()
|
||||
//
|
||||
// // Find vertical distance to the ground.
|
||||
// ctx.scene.pickWithRay(
|
||||
// Ray(pickInfo.pickedMesh.position, DOWN_VECTOR),
|
||||
// { it.isEnabled() && it.metadata is CollisionUserData },
|
||||
// )?.let { groundPick ->
|
||||
// dragAdjust.y -= groundPick.distance
|
||||
// }
|
||||
//
|
||||
// return Pick(
|
||||
// entity,
|
||||
// pickInfo.pickedMesh,
|
||||
// grabOffset,
|
||||
// dragAdjust,
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
private class TranslationState(
|
||||
|
@ -6,6 +6,5 @@ import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
|
||||
class QuestEditorRendererWidget(
|
||||
scope: CoroutineScope,
|
||||
canvas: HTMLCanvasElement,
|
||||
renderer: QuestRenderer,
|
||||
) : QuestRendererWidget(scope, canvas, renderer)
|
||||
) : QuestRendererWidget(scope, renderer)
|
||||
|
@ -62,7 +62,7 @@ class QuestEditorWidget(
|
||||
),
|
||||
)
|
||||
),
|
||||
DockedRow(
|
||||
DockedStack(
|
||||
flex = 9,
|
||||
items = listOf(
|
||||
DockedWidget(
|
||||
|
@ -1,7 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.widgets.RendererWidget
|
||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
@ -10,7 +9,6 @@ import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
abstract class QuestRendererWidget(
|
||||
scope: CoroutineScope,
|
||||
private val canvas: HTMLCanvasElement,
|
||||
private val renderer: QuestRenderer,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() =
|
||||
@ -18,7 +16,7 @@ abstract class QuestRendererWidget(
|
||||
className = "pw-quest-editor-quest-renderer"
|
||||
tabIndex = -1
|
||||
|
||||
addChild(RendererWidget(scope, canvas, renderer))
|
||||
addChild(RendererWidget(scope, renderer))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,13 +1,14 @@
|
||||
package world.phantasmal.web.viewer
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.core.PwTool
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.widgets.RendererWidget
|
||||
import world.phantasmal.web.viewer.controller.ViewerController
|
||||
import world.phantasmal.web.viewer.controller.ViewerToolbarController
|
||||
import world.phantasmal.web.viewer.rendering.MeshRenderer
|
||||
import world.phantasmal.web.viewer.rendering.TextureRenderer
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import world.phantasmal.web.viewer.widgets.ViewerToolbar
|
||||
import world.phantasmal.web.viewer.widgets.ViewerWidget
|
||||
@ -15,7 +16,7 @@ import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class Viewer(
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
private val createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : DisposableContainer(), PwTool {
|
||||
override val toolType = PwToolType.Viewer
|
||||
|
||||
@ -24,19 +25,24 @@ class Viewer(
|
||||
val viewerStore = addDisposable(ViewerStore(scope))
|
||||
|
||||
// Controllers
|
||||
val viewerController = addDisposable(ViewerController())
|
||||
val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
||||
|
||||
// Rendering
|
||||
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
|
||||
canvas.style.outline = "none"
|
||||
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
|
||||
val meshRenderer = addDisposable(
|
||||
MeshRenderer(viewerStore, createThreeRenderer)
|
||||
)
|
||||
val textureRenderer = addDisposable(
|
||||
TextureRenderer(viewerStore, createThreeRenderer)
|
||||
)
|
||||
|
||||
// Main Widget
|
||||
return ViewerWidget(
|
||||
scope,
|
||||
viewerController,
|
||||
{ s -> ViewerToolbar(s, viewerToolbarController) },
|
||||
canvas,
|
||||
renderer
|
||||
{ s -> RendererWidget(s, meshRenderer) },
|
||||
{ s -> RendererWidget(s, textureRenderer) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package world.phantasmal.web.viewer.controller
|
||||
|
||||
import world.phantasmal.webui.controllers.Tab
|
||||
import world.phantasmal.webui.controllers.TabController
|
||||
|
||||
sealed class ViewerTab(override val title: String) : Tab {
|
||||
object Mesh : ViewerTab("Model")
|
||||
object Texture : ViewerTab("Texture")
|
||||
}
|
||||
|
||||
class ViewerController : TabController<ViewerTab>(
|
||||
listOf(ViewerTab.Mesh, ViewerTab.Texture)
|
||||
)
|
@ -10,6 +10,7 @@ import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseNj
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseXj
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseXvm
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
@ -26,8 +27,10 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
val resultMessage: Val<String> = result.map {
|
||||
if (it is Failure) "An error occurred while opening files."
|
||||
else "Encountered some problems while opening files."
|
||||
when (it) {
|
||||
is Success, null -> "Encountered some problems while opening files."
|
||||
is Failure -> "An error occurred while opening files."
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openFiles(files: List<File>) {
|
||||
@ -66,6 +69,19 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
}
|
||||
}
|
||||
|
||||
"xvm" -> {
|
||||
if (textureFound) continue
|
||||
|
||||
textureFound = true
|
||||
val xvmResult = parseXvm(readFile(file).cursor(Endianness.Little))
|
||||
result.addResult(xvmResult)
|
||||
|
||||
if (xvmResult is Success) {
|
||||
store.setCurrentTextures(xvmResult.value.textures)
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.addProblem(
|
||||
Severity.Error,
|
||||
|
@ -1,63 +1,53 @@
|
||||
package world.phantasmal.web.viewer.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.lib.fileFormats.ninja.XvrTexture
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData
|
||||
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.babylon.Mesh
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToMesh
|
||||
import world.phantasmal.web.core.rendering.disposeObject3DResources
|
||||
import world.phantasmal.web.externals.three.BufferGeometry
|
||||
import world.phantasmal.web.externals.three.Mesh
|
||||
import world.phantasmal.web.externals.three.PerspectiveCamera
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import kotlin.math.PI
|
||||
|
||||
class MeshRenderer(
|
||||
store: ViewerStore,
|
||||
canvas: HTMLCanvasElement,
|
||||
engine: Engine,
|
||||
) : Renderer(canvas, engine) {
|
||||
createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : Renderer(
|
||||
createThreeRenderer,
|
||||
PerspectiveCamera(
|
||||
fov = 45.0,
|
||||
aspect = 1.0,
|
||||
near = 1.0,
|
||||
far = 1_000.0,
|
||||
)
|
||||
) {
|
||||
private var mesh: Mesh? = null
|
||||
|
||||
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 3, 70.0, Vector3.Zero(), scene)
|
||||
|
||||
init {
|
||||
with(camera) {
|
||||
attachControl(
|
||||
canvas,
|
||||
noPreventDefault = false,
|
||||
useCtrlForPanning = false,
|
||||
panningMouseButton = 0
|
||||
)
|
||||
inertia = 0.0
|
||||
angularSensibilityX = 200.0
|
||||
angularSensibilityY = 200.0
|
||||
panningInertia = 0.0
|
||||
panningSensibility = 10.0
|
||||
panningAxis = Vector3(1.0, 1.0, 0.0)
|
||||
pinchDeltaPercentage = 0.1
|
||||
wheelDeltaPercentage = 0.1
|
||||
camera.position.set(0.0, 50.0, 200.0)
|
||||
controls.update()
|
||||
|
||||
controls.screenSpacePanning = true
|
||||
|
||||
observe(store.currentNinjaObject, store.currentTextures, ::ninjaObjectOrXvmChanged)
|
||||
}
|
||||
|
||||
private fun ninjaObjectOrXvmChanged(ninjaObject: NinjaObject<*>?, textures: List<XvrTexture>) {
|
||||
mesh?.let { mesh ->
|
||||
disposeObject3DResources(mesh)
|
||||
scene.remove(mesh)
|
||||
}
|
||||
|
||||
observe(store.currentNinjaObject, ::ninjaObjectOrXvmChanged)
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
mesh?.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
private fun ninjaObjectOrXvmChanged(ninjaObject: NinjaObject<*>?) {
|
||||
mesh?.dispose()
|
||||
|
||||
if (ninjaObject != null) {
|
||||
val mesh = Mesh("Model", scene)
|
||||
val vertexData = ninjaObjectToVertexData(ninjaObject)
|
||||
vertexData.applyToMesh(mesh)
|
||||
val mesh = ninjaObjectToMesh(ninjaObject, textures, boundingVolumes = true)
|
||||
|
||||
// Make sure we rotate around the center of the model instead of its origin.
|
||||
val bb = mesh.getBoundingInfo().boundingBox
|
||||
val height = bb.maximum.y - bb.minimum.y
|
||||
mesh.position = mesh.position.addInPlaceFromFloats(0.0, -height / 2 - bb.minimum.y, 0.0)
|
||||
val bb = (mesh.geometry as BufferGeometry).boundingBox!!
|
||||
val height = bb.max.y - bb.min.y
|
||||
mesh.translateY(-height / 2 - bb.min.y)
|
||||
scene.add(mesh)
|
||||
|
||||
this.mesh = mesh
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
package world.phantasmal.web.viewer.rendering
|
||||
|
||||
import world.phantasmal.lib.fileFormats.ninja.XvrTexture
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.rendering.disposeObject3DResources
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import world.phantasmal.webui.obj
|
||||
|
||||
class TextureRenderer(
|
||||
store: ViewerStore,
|
||||
createThreeRenderer: () -> DisposableThreeRenderer,
|
||||
) : Renderer(
|
||||
createThreeRenderer,
|
||||
OrthographicCamera(
|
||||
left = -400.0,
|
||||
right = 400.0,
|
||||
top = 300.0,
|
||||
bottom = -300.0,
|
||||
near = 1.0,
|
||||
far = 10.0,
|
||||
)
|
||||
) {
|
||||
private var meshes = listOf<Mesh>()
|
||||
|
||||
init {
|
||||
observe(store.currentTextures, ::texturesChanged)
|
||||
}
|
||||
|
||||
private fun texturesChanged(textures: List<XvrTexture>) {
|
||||
meshes.forEach { mesh ->
|
||||
disposeObject3DResources(mesh)
|
||||
scene.remove(mesh)
|
||||
}
|
||||
|
||||
var x = 0.0
|
||||
|
||||
meshes = textures.map { xvr ->
|
||||
val quad = Mesh(
|
||||
createQuad(x, 0.0, xvr.width, xvr.height),
|
||||
MeshBasicMaterial(obj {
|
||||
map = xvrTextureToThree(xvr, filter = NearestFilter)
|
||||
transparent = true
|
||||
})
|
||||
)
|
||||
scene.add(quad)
|
||||
|
||||
x += xvr.width + 10.0
|
||||
|
||||
quad
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQuad(x: Double, y: Double, width: Int, height: Int): PlaneGeometry {
|
||||
val quad = PlaneGeometry(
|
||||
width.toDouble(),
|
||||
height.toDouble(),
|
||||
widthSegments = 1.0,
|
||||
heightSegments = 1.0
|
||||
)
|
||||
quad.faceVertexUvs = arrayOf(
|
||||
arrayOf(
|
||||
arrayOf(Vector2(0.0, 0.0), Vector2(0.0, 1.0), Vector2(1.0, 0.0)),
|
||||
arrayOf(Vector2(0.0, 1.0), Vector2(1.0, 1.0), Vector2(1.0, 0.0)),
|
||||
)
|
||||
)
|
||||
quad.translate(x + width / 2, y + height / 2, -5.0)
|
||||
return quad
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package world.phantasmal.web.viewer.rendering
|
||||
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.set
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ninja.XvrTexture
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.webui.obj
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun xvrTextureToThree(xvr: XvrTexture, filter: TextureFilter = LinearFilter): Texture {
|
||||
val format: CompressedPixelFormat
|
||||
val dataSize: Int
|
||||
|
||||
when (xvr.format.second) {
|
||||
6 -> {
|
||||
format = RGBA_S3TC_DXT1_Format
|
||||
dataSize = (xvr.width * xvr.height) / 2
|
||||
}
|
||||
7 -> {
|
||||
format = RGBA_S3TC_DXT3_Format
|
||||
dataSize = xvr.width * xvr.height
|
||||
}
|
||||
else -> error("Format ${xvr.format.first}, ${xvr.format.second} not supported.")
|
||||
}
|
||||
|
||||
val texture = CompressedTexture(
|
||||
arrayOf(obj {
|
||||
data = Uint8Array(xvr.data.arrayBuffer, 0, dataSize)
|
||||
width = xvr.width
|
||||
height = xvr.height
|
||||
}),
|
||||
xvr.width,
|
||||
xvr.height,
|
||||
format,
|
||||
wrapS = MirroredRepeatWrapping,
|
||||
wrapT = MirroredRepeatWrapping,
|
||||
magFilter = filter,
|
||||
minFilter = filter,
|
||||
)
|
||||
texture.needsUpdate = true
|
||||
return texture
|
||||
}
|
||||
|
||||
private fun xvrTextureToUint8Array(xvr: XvrTexture): Uint8Array {
|
||||
val dataSize = when (xvr.format.second) {
|
||||
6 -> (xvr.width * xvr.height) / 2
|
||||
7 -> xvr.width * xvr.height
|
||||
else -> error("Format ${xvr.format.first}, ${xvr.format.second} not supported.")
|
||||
}
|
||||
|
||||
val cursor = xvr.data.cursor(size = dataSize)
|
||||
val image = Uint8Array(xvr.width * xvr.height * 4)
|
||||
|
||||
val stride = 4 * xvr.width
|
||||
var i = 0
|
||||
|
||||
while (cursor.hasBytesLeft(8)) {
|
||||
// Each block of 4 x 4 pixels is compressed to 8 bytes.
|
||||
val c0 = cursor.uShort().toInt() // Color 0
|
||||
val c1 = cursor.uShort().toInt() // Color 1
|
||||
val codes = cursor.int() // A 2-bit code per pixel.
|
||||
|
||||
// Extract color components and normalize them to the range [0, 1].
|
||||
val c0r = (c0 ushr 11) / 31.0
|
||||
val c0g = ((c0 ushr 5) and 0x3F) / 63.0
|
||||
val c0b = (c0 and 0x1F) / 31.0
|
||||
|
||||
val c1r = (c1 ushr 11) / 31.0
|
||||
val c1g = ((c1 ushr 5) and 0x3F) / 63.0
|
||||
val c1b = (c1 and 0x1F) / 31.0
|
||||
|
||||
// Loop over the codes.
|
||||
for (j in 0 until 16) {
|
||||
val shift = 2 * (16 - j - 1)
|
||||
val r: Double
|
||||
val g: Double
|
||||
val b: Double
|
||||
val a: Double
|
||||
|
||||
when ((codes ushr shift) and 0b11) {
|
||||
0 -> {
|
||||
r = c0r
|
||||
g = c0g
|
||||
b = c0b
|
||||
a = 1.0
|
||||
}
|
||||
1 -> {
|
||||
r = c1r
|
||||
g = c1g
|
||||
b = c1b
|
||||
a = 1.0
|
||||
}
|
||||
2 -> {
|
||||
if (c0 > c1) {
|
||||
r = (2 * c0r + c1r) / 3
|
||||
g = (2 * c0g + c1g) / 3
|
||||
b = (2 * c0b + c1b) / 3
|
||||
a = 1.0
|
||||
} else {
|
||||
r = (c0r + c1r) / 2
|
||||
g = (c0g + c1g) / 2
|
||||
b = (c0b + c1b) / 2
|
||||
a = 1.0
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
if (c0 > c1) {
|
||||
r = (c0r + 2 * c1r) / 3
|
||||
g = (c0g + 2 * c1g) / 3
|
||||
b = (c0b + 2 * c1b) / 3
|
||||
a = 1.0
|
||||
} else {
|
||||
r = 0.0
|
||||
g = 0.0
|
||||
b = 0.0
|
||||
a = 0.0
|
||||
}
|
||||
}
|
||||
// Unreachable case.
|
||||
else -> error("Invalid code.")
|
||||
}
|
||||
|
||||
// Block-relative pixel coordinates.
|
||||
val blockX = 3 - j % 4
|
||||
val blockY = 3 - j / 4
|
||||
// Offset into the image array.
|
||||
val offset = i + (4 * blockX + blockY * stride)
|
||||
image[offset] = (r * 255).roundToInt().toByte()
|
||||
image[offset + 1] = (g * 255).roundToInt().toByte()
|
||||
image[offset + 2] = (b * 255).roundToInt().toByte()
|
||||
image[offset + 3] = (a * 255).roundToInt().toByte()
|
||||
}
|
||||
|
||||
// Jump ahead 4 pixels.
|
||||
i += 16
|
||||
|
||||
if (i % stride == 0) {
|
||||
// Jump ahead 4 rows.
|
||||
i += 3 * stride
|
||||
}
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
@ -2,16 +2,25 @@ package world.phantasmal.web.viewer.store
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.lib.fileFormats.ninja.XvrTexture
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
class ViewerStore(scope: CoroutineScope) : Store(scope) {
|
||||
private val _currentNinjaObject = mutableVal<NinjaObject<*>?>(null)
|
||||
private val _currentTextures = mutableListVal<XvrTexture>(mutableListOf())
|
||||
|
||||
val currentNinjaObject: Val<NinjaObject<*>?> = _currentNinjaObject
|
||||
val currentTextures: ListVal<XvrTexture> = _currentTextures
|
||||
|
||||
fun setCurrentNinjaObject(ninjaObject: NinjaObject<*>?) {
|
||||
_currentNinjaObject.value = ninjaObject
|
||||
}
|
||||
|
||||
fun setCurrentTextures(textures: List<XvrTexture>) {
|
||||
_currentTextures.value = textures
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.web.viewer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.widgets.RendererWidget
|
||||
import world.phantasmal.web.viewer.controller.ViewerController
|
||||
import world.phantasmal.web.viewer.controller.ViewerTab
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.TabContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
/**
|
||||
@ -13,20 +13,22 @@ import world.phantasmal.webui.widgets.Widget
|
||||
*/
|
||||
class ViewerWidget(
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: ViewerController,
|
||||
private val createToolbar: (CoroutineScope) -> Widget,
|
||||
private val canvas: HTMLCanvasElement,
|
||||
private val renderer: Renderer,
|
||||
private val createMeshWidget: (CoroutineScope) -> Widget,
|
||||
private val createTextureWidget: (CoroutineScope) -> Widget,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-viewer-viewer"
|
||||
|
||||
addChild(createToolbar(scope))
|
||||
div {
|
||||
className = "pw-viewer-viewer-container"
|
||||
|
||||
addChild(RendererWidget(scope, canvas, renderer))
|
||||
}
|
||||
addChild(TabContainer(scope, ctrl = ctrl, createWidget = { scope, tab ->
|
||||
when (tab) {
|
||||
ViewerTab.Mesh -> createMeshWidget(scope)
|
||||
ViewerTab.Texture -> createTextureWidget(scope)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -38,10 +40,8 @@ class ViewerWidget(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pw-viewer-viewer-container {
|
||||
.pw-viewer-viewer > .pw-tab-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
""".trimIndent())
|
||||
|
Loading…
Reference in New Issue
Block a user