polish: deduplicate model+cape stuff and fix layout

This commit is contained in:
Calum H. authored and Alejandro González committed 2025-06-14 17:46:45 +02:00
1 parent 187e892c18
commit fc929bfca3
7 files changed
+362 -256

No files matched your search

@@ -1,9 +1,11 @@
import * as THREE from 'three'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import type { Skin, Cape } from '../skins'
import { determineModelType } from '../skins'
import { reactive } from 'vue'
import {
setupSkinModel,
disposeCaches
} from '@modrinth/utils'
export interface RenderResult {
forwards: string
@@ -14,12 +16,7 @@ class BatchSkinRenderer {
private renderer: THREE.WebGLRenderer
private readonly scene: THREE.Scene
private readonly camera: THREE.PerspectiveCamera
private modelCache: Map<string, GLTF> = new Map()
private textureCache: Map<string, THREE.Texture> = new Map()
private currentModel: THREE.Group | null = null
private capeModel: THREE.Object3D | null = null
private bodyNode: THREE.Object3D | null = null
private capeAttached: boolean = false
constructor(width: number = 360, height: number = 504) {
const canvas = document.createElement('canvas')
@@ -45,102 +42,6 @@ class BatchSkinRenderer {
this.scene.add(ambientLight)
}
private async loadModel(modelUrl: string): Promise<GLTF> {
if (this.modelCache.has(modelUrl)) {
return this.modelCache.get(modelUrl)!
}
const loader = new GLTFLoader()
return new Promise<GLTF>((resolve, reject) => {
loader.load(
modelUrl,
(gltf) => {
this.modelCache.set(modelUrl, gltf)
resolve(gltf)
},
undefined,
reject,
)
})
}
private async loadTexture(textureUrl: string): Promise<THREE.Texture> {
if (this.textureCache.has(textureUrl)) {
return this.textureCache.get(textureUrl)!
}
return new Promise<THREE.Texture>((resolve) => {
const textureLoader = new THREE.TextureLoader()
textureLoader.load(textureUrl, (texture) => {
texture.colorSpace = THREE.SRGBColorSpace
texture.flipY = false
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
this.textureCache.set(textureUrl, texture)
resolve(texture)
})
})
}
private applyTexture(model: THREE.Object3D, texture: THREE.Texture): void {
model.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
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
}
})
}
})
}
private applyCapeTexture(model: THREE.Object3D, texture: 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
mat.metalness = 0
mat.color.set(0xffffff)
mat.toneMapped = false
mat.roughness = 1
mat.side = THREE.DoubleSide
mat.needsUpdate = true
}
})
}
})
}
private attachCapeToBody(): void {
if (!this.bodyNode || !this.capeModel || this.capeAttached) return
if (this.capeModel.parent) {
this.capeModel.parent.remove(this.capeModel)
}
this.capeModel.position.set(0, -1, -0.01)
this.capeModel.rotation.set(0, -Math.PI / 2, 0)
this.bodyNode.add(this.capeModel)
this.capeAttached = true
}
public async renderSkin(
textureUrl: string,
modelUrl: string,
@@ -200,22 +101,13 @@ class BatchSkinRenderer {
this.scene.remove(this.currentModel)
}
this.bodyNode = null
this.capeAttached = false
const [gltf, texture] = await Promise.all([
this.loadModel(modelUrl),
this.loadTexture(textureUrl),
])
const model = gltf.scene.clone()
this.applyTexture(model, texture)
model.traverse((node) => {
if (node.name === 'Body') {
this.bodyNode = node
}
})
// Use the utility function to setup the skin model
const { model } = await setupSkinModel(
modelUrl,
textureUrl,
capeModelUrl,
capeUrl
)
const group = new THREE.Group()
group.add(model)
@@ -224,30 +116,11 @@ class BatchSkinRenderer {
this.scene.add(group)
this.currentModel = group
if (capeModelUrl && capeUrl) {
const [capeGltf, capeTexture] = await Promise.all([
this.loadModel(capeModelUrl),
this.loadTexture(capeUrl),
])
this.capeModel = capeGltf.scene.clone()
this.applyCapeTexture(this.capeModel, capeTexture)
if (this.bodyNode && this.capeModel) {
this.attachCapeToBody()
}
}
}
public dispose(): void {
Array.from(this.textureCache.values()).forEach((texture) => {
texture.dispose()
})
this.renderer.dispose()
this.textureCache.clear()
this.modelCache.clear()
disposeCaches()
}
}
+2 -1
View File
@@ -418,6 +418,7 @@ $skin-card-gap: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-left: calc((2.5rem / 2));
@media (max-width: 700px) {
height: 50vh;
@@ -430,7 +431,7 @@ $skin-card-gap: 4px;
.skin-card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(6, 1fr);
gap: $skin-card-gap;
width: 100%;