polish: deduplicate model+cape stuff and fix layout
This commit is contained in:
1 parent
187e892c18
commit
fc929bfca3
7 files changed
+362
-256
No files matched your search
@@ -8,3 +8,4 @@ export * from './types'
|
||||
export * from './users'
|
||||
export * from './utils'
|
||||
export * from './servers'
|
||||
export * from './three/skin-rendering'
|
||||
@@ -20,10 +20,12 @@
|
||||
"@codemirror/state": "^6.3.2",
|
||||
"@codemirror/view": "^6.22.1",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"highlight.js": "^11.9.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"ofetch": "^1.3.4",
|
||||
"three": "^0.172.0",
|
||||
"xss": "^1.0.14"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import * as THREE from 'three'
|
||||
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
|
||||
export interface SkinRendererConfig {
|
||||
textureColorSpace?: THREE.ColorSpace
|
||||
textureFlipY?: boolean
|
||||
textureMagFilter?: THREE.MagnificationTextureFilter
|
||||
textureMinFilter?: THREE.MinificationTextureFilter
|
||||
}
|
||||
|
||||
// Private caches
|
||||
const modelCache: Map<string, GLTF> = new Map()
|
||||
const textureCache: Map<string, THREE.Texture> = new Map()
|
||||
|
||||
/**
|
||||
* Load and cache a GLTF model
|
||||
*/
|
||||
export async function loadModel(modelUrl: string): Promise<GLTF> {
|
||||
if (modelCache.has(modelUrl)) {
|
||||
return modelCache.get(modelUrl)!
|
||||
}
|
||||
|
||||
const loader = new GLTFLoader()
|
||||
return new Promise<GLTF>((resolve, reject) => {
|
||||
loader.load(
|
||||
modelUrl,
|
||||
(gltf) => {
|
||||
modelCache.set(modelUrl, gltf)
|
||||
resolve(gltf)
|
||||
},
|
||||
undefined,
|
||||
reject,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and configure a texture with skin-specific settings
|
||||
*/
|
||||
export async function loadTexture(
|
||||
textureUrl: string,
|
||||
config: SkinRendererConfig = {}
|
||||
): Promise<THREE.Texture> {
|
||||
const cacheKey = `${textureUrl}_${JSON.stringify(config)}`
|
||||
|
||||
if (textureCache.has(cacheKey)) {
|
||||
return textureCache.get(cacheKey)!
|
||||
}
|
||||
|
||||
return new Promise<THREE.Texture>((resolve) => {
|
||||
const textureLoader = new THREE.TextureLoader()
|
||||
textureLoader.load(textureUrl, (texture) => {
|
||||
texture.colorSpace = config.textureColorSpace ?? THREE.SRGBColorSpace
|
||||
texture.flipY = config.textureFlipY ?? false
|
||||
texture.magFilter = config.textureMagFilter ?? THREE.NearestFilter
|
||||
texture.minFilter = config.textureMinFilter ?? THREE.NearestFilter
|
||||
|
||||
textureCache.set(cacheKey, texture)
|
||||
resolve(texture)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply skin texture to a model, excluding cape meshes
|
||||
*/
|
||||
export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): void {
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh
|
||||
|
||||
// Skip cape meshes
|
||||
if (mesh.name === 'Cape') return
|
||||
|
||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
|
||||
materials.forEach((mat: THREE.Material) => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.map = texture
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply cape texture with specific settings for cape materials
|
||||
*/
|
||||
export function applyCapeTexture(
|
||||
model: THREE.Object3D,
|
||||
texture: THREE.Texture | null,
|
||||
transparentTexture?: THREE.Texture
|
||||
): void {
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh
|
||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
|
||||
materials.forEach((mat: THREE.Material) => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.map = texture || transparentTexture || null
|
||||
mat.transparent = transparentTexture ? true : false
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.roughness = 1
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.needsUpdate = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach cape model to the body node with standard positioning
|
||||
*/
|
||||
export function attachCapeToBody(
|
||||
bodyNode: THREE.Object3D,
|
||||
capeModel: THREE.Object3D,
|
||||
position = { x: 0, y: -1, z: -0.01 },
|
||||
rotation = { x: 0, y: -Math.PI / 2, z: 0 }
|
||||
): void {
|
||||
if (!bodyNode || !capeModel) return
|
||||
|
||||
// Remove cape from current parent if it exists
|
||||
if (capeModel.parent) {
|
||||
capeModel.parent.remove(capeModel)
|
||||
}
|
||||
|
||||
// Set cape position and rotation
|
||||
capeModel.position.set(position.x, position.y, position.z)
|
||||
capeModel.rotation.set(rotation.x, rotation.y, rotation.z)
|
||||
|
||||
// Attach to body
|
||||
bodyNode.add(capeModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the body node in a model hierarchy
|
||||
*/
|
||||
export function findBodyNode(model: THREE.Object3D): THREE.Object3D | null {
|
||||
let bodyNode: THREE.Object3D | null = null
|
||||
|
||||
model.traverse((node) => {
|
||||
if (node.name === 'Body') {
|
||||
bodyNode = node
|
||||
}
|
||||
})
|
||||
|
||||
return bodyNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transparent texture for use as fallback
|
||||
*/
|
||||
export function createTransparentTexture(): THREE.Texture {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = canvas.height = 1
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
ctx.clearRect(0, 0, 1, 1)
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
texture.needsUpdate = true
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
texture.flipY = false
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a complete skin model with optional cape
|
||||
*/
|
||||
export async function setupSkinModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeModelUrl?: string,
|
||||
capeTextureUrl?: string,
|
||||
config: SkinRendererConfig = {}
|
||||
): Promise<{
|
||||
model: THREE.Object3D
|
||||
bodyNode: THREE.Object3D | null
|
||||
capeModel: THREE.Object3D | null
|
||||
}> {
|
||||
// Load model and texture in parallel
|
||||
const [gltf, texture] = await Promise.all([
|
||||
loadModel(modelUrl),
|
||||
loadTexture(textureUrl, config),
|
||||
])
|
||||
|
||||
const model = gltf.scene.clone()
|
||||
applyTexture(model, texture)
|
||||
|
||||
const bodyNode = findBodyNode(model)
|
||||
let capeModel: THREE.Object3D | null = null
|
||||
|
||||
// Load cape if provided
|
||||
if (capeModelUrl && capeTextureUrl) {
|
||||
const [capeGltf, capeTexture] = await Promise.all([
|
||||
loadModel(capeModelUrl),
|
||||
loadTexture(capeTextureUrl, config),
|
||||
])
|
||||
|
||||
capeModel = capeGltf.scene.clone()
|
||||
applyCapeTexture(capeModel, capeTexture)
|
||||
|
||||
if (bodyNode && capeModel) {
|
||||
attachCapeToBody(bodyNode, capeModel)
|
||||
}
|
||||
}
|
||||
|
||||
return { model, bodyNode, capeModel }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches and dispose of cached resources
|
||||
*/
|
||||
export function disposeCaches(): void {
|
||||
Array.from(textureCache.values()).forEach((texture) => {
|
||||
texture.dispose()
|
||||
})
|
||||
|
||||
textureCache.clear()
|
||||
modelCache.clear()
|
||||
}
|
||||
Reference in new issue
Block a user