You've already forked AstralRinth
forked from didirus/AstralRinth
Skins improvements/fixes (#3943)
* feat: only initialize batch renderer if needed & head storage * feat: support webp storage of skin renders if supported (falls back to png if not) * fix: performance improvements with cache loading+saving * fix: mirrored skins + remove cape model for embedded cape * feat: antialiasing * fix: leg jumping & store fbx's for reference * fix: lint issues * fix: lint issues * feat: tweaks to radial spotlight * fix: app nav btn colors
This commit is contained in:
@@ -83,7 +83,7 @@ export const TwitterIcon = _TwitterIcon
|
||||
export const WindowsIcon = _WindowsIcon
|
||||
export const YouTubeIcon = _YouTubeIcon
|
||||
|
||||
export { default as CapeModel } from './models/cape.gltf?url'
|
||||
// Skin Models
|
||||
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
|
||||
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"asset": { "version": "2.0", "generator": "Blockbench 4.12.4 glTF exporter" },
|
||||
"scenes": [{ "nodes": [1], "name": "blockbench_export" }],
|
||||
"scene": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"rotation": [0, 0, 0.19509032201612825, 0.9807852804032304],
|
||||
"translation": [0.15625, 1, 0],
|
||||
"name": "Cape",
|
||||
"mesh": 0
|
||||
},
|
||||
{ "children": [0] }
|
||||
],
|
||||
"bufferViews": [
|
||||
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962, "byteStride": 12 },
|
||||
{ "buffer": 0, "byteOffset": 288, "byteLength": 288, "target": 34962, "byteStride": 12 },
|
||||
{ "buffer": 0, "byteOffset": 576, "byteLength": 192, "target": 34962, "byteStride": 8 },
|
||||
{ "buffer": 0, "byteOffset": 768, "byteLength": 72, "target": 34963 }
|
||||
],
|
||||
"buffers": [
|
||||
{
|
||||
"byteLength": 840,
|
||||
"uri": "data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"
|
||||
}
|
||||
],
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [0.03125, 0, 0.3125],
|
||||
"min": [-0.03125, -1, -0.3125],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [1, 1, 1],
|
||||
"min": [-1, -1, -1],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 2,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [0.34375, 0.53125],
|
||||
"min": [0, 0],
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 3,
|
||||
"componentType": 5123,
|
||||
"count": 36,
|
||||
"max": [23],
|
||||
"min": [0],
|
||||
"type": "SCALAR"
|
||||
}
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"pbrMetallicRoughness": {
|
||||
"metallicFactor": 0,
|
||||
"roughnessFactor": 1,
|
||||
"baseColorTexture": { "index": 0 }
|
||||
},
|
||||
"alphaMode": "MASK",
|
||||
"alphaCutoff": 0.05,
|
||||
"doubleSided": true
|
||||
}
|
||||
],
|
||||
"textures": [{ "sampler": 0, "source": 0, "name": "cape.png" }],
|
||||
"samplers": [{ "magFilter": 9728, "minFilter": 9728, "wrapS": 33071, "wrapT": 33071 }],
|
||||
"images": [
|
||||
{
|
||||
"mimeType": "image/png",
|
||||
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"primitives": [
|
||||
{
|
||||
"mode": 4,
|
||||
"attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
|
||||
"indices": 3,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
packages/assets/models/classic-player.fbx
Normal file
BIN
packages/assets/models/classic-player.fbx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/assets/models/slim-player.fbx
Normal file
BIN
packages/assets/models/slim-player.fbx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -32,6 +32,7 @@
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@tresjs/cientos": "^4.3.0",
|
||||
"@tresjs/core": "^4.3.4",
|
||||
"@tresjs/post-processing": "^2.4.0",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
@@ -41,6 +42,7 @@
|
||||
"floating-vue": "^5.2.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"postprocessing": "^6.37.6",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"three": "^0.172.0",
|
||||
"vue-multiselect": "3.0.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<TresCanvas
|
||||
shadows
|
||||
alpha
|
||||
:antialias="antialias"
|
||||
:antialias="true"
|
||||
:renderer-options="{
|
||||
outputColorSpace: THREE.SRGBColorSpace,
|
||||
toneMapping: THREE.NoToneMapping,
|
||||
@@ -46,36 +46,39 @@
|
||||
<primitive v-if="scene" :object="scene" />
|
||||
</Group>
|
||||
|
||||
<TresMesh
|
||||
<!-- <TresMesh
|
||||
:position="[0, -0.095 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.5 * 0.75 * scale, 0.5 * 0.75 * scale, 0.5 * 0.75 * scale]"
|
||||
:scale="[0.4 * 0.75 * scale, 0.4 * 0.75 * scale, 0.4 * 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresMeshBasicMaterial
|
||||
color="#000000"
|
||||
:opacity="0.2"
|
||||
:opacity="0.5"
|
||||
transparent
|
||||
:depth-write="false"
|
||||
/>
|
||||
</TresMesh>
|
||||
|
||||
<TresMesh
|
||||
:position="[0, -0.1 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresMeshBasicMaterial
|
||||
:map="radialTexture"
|
||||
transparent
|
||||
:depth-write="false"
|
||||
:blending="THREE.AdditiveBlending"
|
||||
/>
|
||||
</TresMesh>
|
||||
</TresMesh> -->
|
||||
</Group>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<EffectComposerPmndrs>
|
||||
<FXAAPmndrs />
|
||||
</EffectComposerPmndrs>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<TresMesh
|
||||
:position="[0, -0.1 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresShaderMaterial v-bind="radialSpotlightShader" />
|
||||
</TresMesh>
|
||||
</Suspense>
|
||||
|
||||
<TresPerspectiveCamera
|
||||
:make-default.camel="true"
|
||||
:fov="fov"
|
||||
@@ -101,6 +104,7 @@
|
||||
import * as THREE from 'three'
|
||||
import { useGLTF } from '@tresjs/cientos'
|
||||
import { useTexture, TresCanvas, useRenderLoop } from '@tresjs/core'
|
||||
import { EffectComposerPmndrs, FXAAPmndrs } from '@tresjs/post-processing'
|
||||
import {
|
||||
shallowRef,
|
||||
ref,
|
||||
@@ -115,13 +119,11 @@ import {
|
||||
import {
|
||||
applyTexture,
|
||||
applyCapeTexture,
|
||||
attachCapeToBody,
|
||||
findBodyNode,
|
||||
createTransparentTexture,
|
||||
loadTexture as loadSkinTexture,
|
||||
} from '@modrinth/utils'
|
||||
import { useDynamicFontSize } from '../../composables'
|
||||
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
|
||||
interface AnimationConfig {
|
||||
baseAnimation: string
|
||||
@@ -136,7 +138,6 @@ const props = withDefaults(
|
||||
capeSrc?: string
|
||||
variant?: 'SLIM' | 'CLASSIC' | 'UNKNOWN'
|
||||
nametag?: string
|
||||
antialias?: boolean
|
||||
scale?: number
|
||||
fov?: number
|
||||
initialRotation?: number
|
||||
@@ -144,7 +145,6 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
variant: 'CLASSIC',
|
||||
antialias: false,
|
||||
scale: 1,
|
||||
fov: 40,
|
||||
capeSrc: undefined,
|
||||
@@ -177,9 +177,6 @@ const selectedModelSrc = computed(() =>
|
||||
)
|
||||
|
||||
const scene = shallowRef<THREE.Object3D | null>(null)
|
||||
const capeScene = shallowRef<THREE.Object3D | null>(null)
|
||||
const bodyNode = shallowRef<THREE.Object3D | null>(null)
|
||||
const capeAttached = ref(false)
|
||||
const lastCapeSrc = ref<string | undefined>(undefined)
|
||||
const texture = shallowRef<THREE.Texture | null>(null)
|
||||
const capeTexture = shallowRef<THREE.Texture | null>(null)
|
||||
@@ -196,6 +193,54 @@ const currentAnimation = ref<string>('')
|
||||
const randomAnimationTimer = ref<number | null>(null)
|
||||
const lastRandomAnimation = ref<string>('')
|
||||
|
||||
const radialSpotlightShader = computed(() => ({
|
||||
uniforms: {
|
||||
innerColor: { value: new THREE.Color(0x000000) },
|
||||
outerColor: { value: new THREE.Color(0xffffff) },
|
||||
innerOpacity: { value: 0.3 },
|
||||
outerOpacity: { value: 0.0 },
|
||||
falloffPower: { value: 1.2 },
|
||||
shadowRadius: { value: 7 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 innerColor;
|
||||
uniform vec3 outerColor;
|
||||
uniform float innerOpacity;
|
||||
uniform float outerOpacity;
|
||||
uniform float falloffPower;
|
||||
uniform float shadowRadius;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = distance(vUv, center) * 2.0;
|
||||
|
||||
// Create shadow in the center
|
||||
float shadowFalloff = 1.0 - smoothstep(0.0, shadowRadius, dist);
|
||||
|
||||
// Create overall spotlight falloff
|
||||
float spotlightFalloff = 1.0 - smoothstep(0.0, 1.0, pow(dist, falloffPower));
|
||||
|
||||
// Combine both effects
|
||||
vec3 color = mix(outerColor, innerColor, shadowFalloff);
|
||||
float opacity = mix(outerOpacity, innerOpacity * shadowFalloff, spotlightFalloff);
|
||||
|
||||
gl_FragColor = vec4(color, opacity);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
}))
|
||||
|
||||
const { baseAnimation, randomAnimations } = toRefs(props.animationConfig)
|
||||
|
||||
function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) {
|
||||
@@ -400,11 +445,9 @@ async function loadModel(src: string) {
|
||||
|
||||
if (texture.value) {
|
||||
applyTexture(scene.value, texture.value)
|
||||
texture.value.needsUpdate = true
|
||||
}
|
||||
|
||||
bodyNode.value = findBodyNode(loadedScene)
|
||||
capeAttached.value = false
|
||||
applyCapeTexture(scene.value, capeTexture.value, transparentTexture)
|
||||
|
||||
if (animations && animations.length > 0) {
|
||||
initializeAnimations(loadedScene, animations)
|
||||
@@ -418,22 +461,6 @@ async function loadModel(src: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCape() {
|
||||
try {
|
||||
const { scene: loadedCape } = await useGLTF(CapeModel)
|
||||
capeScene.value = markRaw(loadedCape)
|
||||
|
||||
applyCapeTexture(capeScene.value, capeTexture.value, transparentTexture)
|
||||
|
||||
if (bodyNode.value && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cape:', error)
|
||||
capeScene.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAndApplyTexture(src: string) {
|
||||
if (!src) return null
|
||||
|
||||
@@ -465,25 +492,9 @@ async function loadAndApplyCapeTexture(src: string | undefined) {
|
||||
capeTexture.value = null
|
||||
}
|
||||
|
||||
if (capeScene.value) {
|
||||
applyCapeTexture(capeScene.value, capeTexture.value, transparentTexture)
|
||||
if (scene.value) {
|
||||
applyCapeTexture(scene.value, capeTexture.value, transparentTexture)
|
||||
}
|
||||
|
||||
if (capeScene.value && bodyNode.value) {
|
||||
if (!src && capeAttached.value && capeScene.value.parent) {
|
||||
capeScene.value.parent.remove(capeScene.value)
|
||||
capeAttached.value = false
|
||||
} else if (src && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachCapeToBodyWrapper() {
|
||||
if (!bodyNode.value || !capeScene.value || capeAttached.value) return
|
||||
|
||||
attachCapeToBody(bodyNode.value, capeScene.value)
|
||||
capeAttached.value = true
|
||||
}
|
||||
|
||||
const centre = ref<[number, number, number]>([0, 1, 0])
|
||||
@@ -539,39 +550,6 @@ function onCanvasClick() {
|
||||
hasDragged.value = false
|
||||
}
|
||||
|
||||
const radialTexture = createRadialTexture(512)
|
||||
radialTexture.minFilter = THREE.LinearFilter
|
||||
radialTexture.magFilter = THREE.LinearFilter
|
||||
radialTexture.wrapS = radialTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||
|
||||
function createRadialTexture(size: number): THREE.CanvasTexture {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = canvas.height = size
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
const grad = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
|
||||
grad.addColorStop(0, 'rgba(119,119,119,0.1)')
|
||||
grad.addColorStop(0.9, 'rgba(255,255,255,0)')
|
||||
ctx.fillStyle = grad
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
return new THREE.CanvasTexture(canvas)
|
||||
}
|
||||
|
||||
watch(
|
||||
[bodyNode, capeScene, isModelLoaded],
|
||||
([newBodyNode, newCapeScene, modelLoaded]) => {
|
||||
if (newBodyNode && newCapeScene && modelLoaded && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(capeScene, (newCapeScene) => {
|
||||
if (newCapeScene && bodyNode.value && isModelLoaded.value && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedModelSrc, (src) => loadModel(src))
|
||||
watch(
|
||||
() => props.textureSrc,
|
||||
@@ -587,7 +565,6 @@ watch(
|
||||
watch(
|
||||
() => props.capeSrc,
|
||||
async (newCapeSrc) => {
|
||||
await loadCape()
|
||||
await loadAndApplyCapeTexture(newCapeSrc)
|
||||
},
|
||||
)
|
||||
@@ -619,8 +596,6 @@ onBeforeMount(async () => {
|
||||
if (props.capeSrc) {
|
||||
await loadAndApplyCapeTexture(props.capeSrc)
|
||||
}
|
||||
|
||||
await loadCape()
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize skin preview:', error)
|
||||
}
|
||||
|
||||
@@ -59,25 +59,23 @@ export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): voi
|
||||
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.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.depthWrite = true
|
||||
if (mat.name !== 'cape') {
|
||||
mat.map = texture
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.depthWrite = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -96,41 +94,27 @@ export function applyCapeTexture(
|
||||
|
||||
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.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.depthWrite = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
if (mat.name === 'cape') {
|
||||
mat.map = texture || transparentTexture || null
|
||||
mat.transparent = !texture || transparentTexture ? true : false
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.depthWrite = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.visible = !!texture
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (capeModel.parent) {
|
||||
capeModel.parent.remove(capeModel)
|
||||
}
|
||||
|
||||
capeModel.position.set(position.x, position.y, position.z)
|
||||
capeModel.rotation.set(rotation.x, rotation.y, rotation.z)
|
||||
bodyNode.add(capeModel)
|
||||
}
|
||||
|
||||
export function findBodyNode(model: THREE.Object3D): THREE.Object3D | null {
|
||||
let bodyNode: THREE.Object3D | null = null
|
||||
|
||||
@@ -162,39 +146,25 @@ export function createTransparentTexture(): THREE.Texture {
|
||||
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)
|
||||
}
|
||||
if (capeTextureUrl) {
|
||||
const capeTexture = await loadTexture(capeTextureUrl, config)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
}
|
||||
|
||||
return { model, bodyNode, capeModel }
|
||||
const bodyNode = findBodyNode(model)
|
||||
|
||||
return { model, bodyNode }
|
||||
}
|
||||
|
||||
export function disposeCaches(): void {
|
||||
|
||||
Reference in New Issue
Block a user