fix: ears regressions (#7148)

* temp: fix skins issues

* fix: lint
This commit is contained in:
Calum H.
2026-08-14 16:17:32 +00:00
committed by GitHub
parent f948b5d358
commit 762d9fe4f4
7 changed files with 315 additions and 46 deletions
@@ -6,6 +6,7 @@
@click="onCanvasClick" @click="onCanvasClick"
> >
<div <div
data-skin-preview-debug="controls"
class="absolute left-0 right-0 z-10 flex items-center justify-center pointer-events-none" class="absolute left-0 right-0 z-10 flex items-center justify-center pointer-events-none"
:style="previewControlsPositionStyle" :style="previewControlsPositionStyle"
> >
@@ -18,6 +19,7 @@
</div> </div>
<div <div
v-if="$slots.subtitle" v-if="$slots.subtitle"
data-skin-preview-debug="subtitle"
class="absolute left-0 right-0 z-10 flex items-center justify-center pointer-events-none" class="absolute left-0 right-0 z-10 flex items-center justify-center pointer-events-none"
:style="subtitlePositionStyle" :style="subtitlePositionStyle"
> >
@@ -27,6 +29,7 @@
</div> </div>
<div <div
v-if="nametag || $slots['nametag-badge']" v-if="nametag || $slots['nametag-badge']"
data-skin-preview-debug="nametag"
class="absolute left-1/2 pointer-events-none z-10" class="absolute left-1/2 pointer-events-none z-10"
:style="nametagStyle" :style="nametagStyle"
> >
@@ -267,16 +270,23 @@ const {
}, },
}) })
const { hasEarsFeatures, isModelLoaded, isTextureLoaded, modelCenter, modelSize, scene } = const {
useSkinPreviewScene({ hasEarsFeatures,
selectedModelSrc, isModelLoaded,
textureSrc: toRef(props, 'textureSrc'), isTextureLoaded,
earsTextureSrc: toRef(props, 'earsTextureSrc'), modelCenter,
capeSrc: toRef(props, 'capeSrc'), modelSize,
earsEnabled: toRef(props, 'earsEnabled'), scene,
initializeAnimations, visibleBounds,
cleanupAnimationState, } = useSkinPreviewScene({
}) selectedModelSrc,
textureSrc: toRef(props, 'textureSrc'),
earsTextureSrc: toRef(props, 'earsTextureSrc'),
capeSrc: toRef(props, 'capeSrc'),
earsEnabled: toRef(props, 'earsEnabled'),
initializeAnimations,
cleanupAnimationState,
})
function syncDamageFlashShaderMaterials() { function syncDamageFlashShaderMaterials() {
syncDamageFlashShader(scene.value, damageFlashIntensity.value) syncDamageFlashShader(scene.value, damageFlashIntensity.value)
@@ -310,6 +320,8 @@ const {
subtitleWrapped: isSubtitleWrapped, subtitleWrapped: isSubtitleWrapped,
modelCenter, modelCenter,
modelSize, modelSize,
scene,
visibleBounds,
isModelLoaded, isModelLoaded,
}) })
@@ -52,7 +52,7 @@ export function createRadialSpotlightShader() {
`, `,
transparent: true, transparent: true,
depthWrite: false, depthWrite: false,
depthTest: false, depthTest: true,
} }
} }
@@ -14,6 +14,11 @@ export interface SkinPreviewFitPadding {
left: number left: number
} }
export interface SkinPreviewBounds {
min: SkinPreviewTuple
max: SkinPreviewTuple
}
export interface SkinPreviewFitLock { export interface SkinPreviewFitLock {
containerSize: { containerSize: {
width: number width: number
@@ -23,6 +28,7 @@ export interface SkinPreviewFitLock {
modelSize: SkinPreviewTuple modelSize: SkinPreviewTuple
padding: SkinPreviewFitPadding padding: SkinPreviewFitPadding
rotation: number rotation: number
visibleBounds: SkinPreviewBounds
} }
export type SkinPreviewTuple = [number, number, number] export type SkinPreviewTuple = [number, number, number]
@@ -1939,6 +1939,10 @@ export function removeEarsMod(model: THREE.Object3D | null) {
registry.removeFromParent() registry.removeFromParent()
} }
export function isEarsModFeature(object: THREE.Object3D) {
return object.name.startsWith(EARS_FEATURE_PREFIX)
}
export function applyEarsMod(model: THREE.Object3D, sourceTexture: THREE.Texture, enabled = true) { export function applyEarsMod(model: THREE.Object3D, sourceTexture: THREE.Texture, enabled = true) {
removeEarsMod(model) removeEarsMod(model)
@@ -11,6 +11,7 @@ import {
} from 'vue' } from 'vue'
import type { import type {
SkinPreviewBounds,
SkinPreviewFitLock, SkinPreviewFitLock,
SkinPreviewFitPadding, SkinPreviewFitPadding,
SkinPreviewFraming, SkinPreviewFraming,
@@ -41,8 +42,88 @@ function cloneModelTuple(tuple: SkinPreviewTuple): SkinPreviewTuple {
return [tuple[0], tuple[1], tuple[2]] return [tuple[0], tuple[1], tuple[2]]
} }
function cloneBounds(bounds: SkinPreviewBounds): SkinPreviewBounds {
return {
min: cloneModelTuple(bounds.min),
max: cloneModelTuple(bounds.max),
}
}
const MODEL_ROTATION_AXIS = new THREE.Vector3(0, 1, 0)
type MaybeReadonlyRef<T> = Ref<T> | ComputedRef<T> type MaybeReadonlyRef<T> = Ref<T> | ComputedRef<T>
type SkinPreviewDebugEntry = Record<string, unknown>
type SkinPreviewDebugWindow = Window & {
__SKIN_PREVIEW_DEBUG__?: SkinPreviewDebugEntry[]
}
function serializeRect(rect: DOMRect) {
return {
bottom: rect.bottom,
height: rect.height,
left: rect.left,
right: rect.right,
top: rect.top,
width: rect.width,
x: rect.x,
y: rect.y,
}
}
function serializeElement(element: Element | null) {
if (!(element instanceof HTMLElement)) return null
const style = window.getComputedStyle(element)
return {
rect: serializeRect(element.getBoundingClientRect()),
style: {
bottom: style.bottom,
height: style.height,
left: style.left,
position: style.position,
top: style.top,
transform: style.transform,
width: style.width,
},
}
}
function getVisibleMeshDebugBounds(root: THREE.Object3D | null) {
if (!root) return []
root.updateWorldMatrix(true, true)
const entries: SkinPreviewDebugEntry[] = []
root.traverse((object) => {
const mesh = object as THREE.Mesh
if (!mesh.isMesh || !mesh.geometry || mesh.visible === false) return
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
if (materials.length && materials.every((material) => material.visible === false)) return
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
if (!mesh.geometry.boundingBox) return
const box = mesh.geometry.boundingBox.clone().applyMatrix4(mesh.matrixWorld)
entries.push({
materials: materials.map((material) => ({
name: material.name,
side: material.side,
visible: material.visible,
})),
max: box.max.toArray(),
min: box.min.toArray(),
name: mesh.name,
parent: mesh.parent?.name,
uuid: mesh.uuid,
})
})
return entries
}
export function useSkinPreviewFit({ export function useSkinPreviewFit({
containerElement, containerElement,
fit, fit,
@@ -59,6 +140,8 @@ export function useSkinPreviewFit({
subtitleWrapped, subtitleWrapped,
modelCenter, modelCenter,
modelSize, modelSize,
scene,
visibleBounds,
isModelLoaded, isModelLoaded,
}: { }: {
containerElement: MaybeReadonlyRef<HTMLElement | null> containerElement: MaybeReadonlyRef<HTMLElement | null>
@@ -76,11 +159,15 @@ export function useSkinPreviewFit({
subtitleWrapped: MaybeReadonlyRef<boolean> subtitleWrapped: MaybeReadonlyRef<boolean>
modelCenter: MaybeReadonlyRef<SkinPreviewTuple> modelCenter: MaybeReadonlyRef<SkinPreviewTuple>
modelSize: MaybeReadonlyRef<SkinPreviewTuple> modelSize: MaybeReadonlyRef<SkinPreviewTuple>
scene: MaybeReadonlyRef<THREE.Object3D | null>
visibleBounds: MaybeReadonlyRef<SkinPreviewBounds>
isModelLoaded: MaybeReadonlyRef<boolean> isModelLoaded: MaybeReadonlyRef<boolean>
}) { }) {
const containerSize = ref({ width: 1, height: 1 }) const containerSize = ref({ width: 1, height: 1 })
const fitLock = ref<SkinPreviewFitLock | null>(null) const fitLock = ref<SkinPreviewFitLock | null>(null)
let resizeObserver: ResizeObserver | undefined let resizeObserver: ResizeObserver | undefined
let debugAnimationFrame: number | null = null
const pendingDebugReasons = new Set<string>()
const fitEnabled = computed(() => { const fitEnabled = computed(() => {
if (fit.value !== undefined) return fit.value if (fit.value !== undefined) return fit.value
@@ -113,6 +200,11 @@ export function useSkinPreviewFit({
const fitModelRotation = computed(() => const fitModelRotation = computed(() =>
lockFitEnabled.value ? (fitLock.value?.rotation ?? modelRotation.value) : modelRotation.value, lockFitEnabled.value ? (fitLock.value?.rotation ?? modelRotation.value) : modelRotation.value,
) )
const fitVisibleBounds = computed(() =>
lockFitEnabled.value
? (fitLock.value?.visibleBounds ?? visibleBounds.value)
: visibleBounds.value,
)
const resolvedFitPadding = computed<SkinPreviewFitPadding>(() => { const resolvedFitPadding = computed<SkinPreviewFitPadding>(() => {
const preset = FRAMING_PRESETS[currentFraming.value].padding const preset = FRAMING_PRESETS[currentFraming.value].padding
@@ -198,22 +290,42 @@ export function useSkinPreviewFit({
} }
}) })
const modelFeetTop = computed(() => { const projectedVisibleBounds = computed(() => {
if (!fitEnabled.value) return null if (!fitEnabled.value) return null
const width = Math.max(containerSize.value.width, 1)
const height = Math.max(containerSize.value.height, 1) const height = Math.max(containerSize.value.height, 1)
const [, sizeY] = fitModelSize.value
const { fov: resolvedFov, position, target } = cameraConfig.value const { fov: resolvedFov, position, target } = cameraConfig.value
const distance = Math.max(Math.abs(position[2] - target[2]), 0.001) const camera = new THREE.PerspectiveCamera(resolvedFov, width / height, 0.1, 1000)
const verticalFov = THREE.MathUtils.degToRad(resolvedFov) camera.position.set(...position)
const modelFeetY = -sizeY / 2 camera.lookAt(...target)
const projectedY = camera.updateProjectionMatrix()
(modelFeetY - target[1]) / distance / Math.max(Math.tan(verticalFov / 2), 0.001) camera.updateMatrixWorld(true)
const topPercent = THREE.MathUtils.clamp(((1 - projectedY) / 2) * 100, 0, 100)
return (topPercent / 100) * height const bounds = fitVisibleBounds.value
const [offsetX, offsetY, offsetZ] = modelOffset.value
let top = Number.POSITIVE_INFINITY
let bottom = Number.NEGATIVE_INFINITY
for (const x of [bounds.min[0], bounds.max[0]]) {
for (const y of [bounds.min[1], bounds.max[1]]) {
for (const z of [bounds.min[2], bounds.max[2]]) {
const point = new THREE.Vector3(x + offsetX, y + offsetY, z + offsetZ)
point.applyAxisAngle(MODEL_ROTATION_AXIS, fitModelRotation.value)
point.project(camera)
const screenY = ((1 - point.y) / 2) * height
top = Math.min(top, screenY)
bottom = Math.max(bottom, screenY)
}
}
}
return { bottom, top }
}) })
const modelFeetTop = computed(() => projectedVisibleBounds.value?.bottom ?? null)
const previewControlsTop = computed(() => const previewControlsTop = computed(() =>
modelFeetTop.value === null ? null : modelFeetTop.value + PREVIEW_CONTROLS_FOOT_OFFSET, modelFeetTop.value === null ? null : modelFeetTop.value + PREVIEW_CONTROLS_FOOT_OFFSET,
) )
@@ -250,22 +362,8 @@ export function useSkinPreviewFit({
const nametagTop = computed(() => { const nametagTop = computed(() => {
if (!fitEnabled.value) return '18%' if (!fitEnabled.value) return '18%'
const height = Math.max(containerSize.value.height, 1) const top = projectedVisibleBounds.value?.top
const [sizeX, sizeY, sizeZ] = fitModelSize.value return top === undefined ? '18%' : `${top - NAMETAG_HEAD_OFFSET}px`
const { fov: resolvedFov, position, target } = cameraConfig.value
const verticalFov = THREE.MathUtils.degToRad(resolvedFov)
const modelTopY = sizeY / 2
const halfX = sizeX / 2
const halfZ = sizeZ / 2
const sinRotation = Math.sin(fitModelRotation.value)
const cosRotation = Math.cos(fitModelRotation.value)
const modelTopZ = -Math.abs(halfX * sinRotation) - Math.abs(halfZ * cosRotation)
const distance = Math.max(Math.abs(position[2] - target[2]) + modelTopZ, 0.001)
const projectedY =
(modelTopY - target[1]) / distance / Math.max(Math.tan(verticalFov / 2), 0.001)
const topPercent = ((1 - projectedY) / 2) * 100
return `${(topPercent / 100) * height - NAMETAG_HEAD_OFFSET}px`
}) })
const spotlightY = computed(() => { const spotlightY = computed(() => {
@@ -292,6 +390,89 @@ export function useSkinPreviewFit({
return [radius, radius, radius] return [radius, radius, radius]
}) })
function captureDebugSnapshot(reasons: string[]) {
if (typeof window === 'undefined') return
const container = containerElement.value
const canvas = container?.querySelector('canvas') ?? null
const debugWindow = window as SkinPreviewDebugWindow
const entries = (debugWindow.__SKIN_PREVIEW_DEBUG__ ??= [])
const snapshot = JSON.parse(
JSON.stringify({
camera: cameraConfig.value,
currentInputs: {
isModelLoaded: isModelLoaded.value,
modelCenter: modelCenter.value,
modelSize: modelSize.value,
padding: resolvedFitPadding.value,
rotation: modelRotation.value,
visibleBounds: visibleBounds.value,
},
dom: {
canvas: {
...serializeElement(canvas),
bufferHeight: canvas instanceof HTMLCanvasElement ? canvas.height : null,
bufferWidth: canvas instanceof HTMLCanvasElement ? canvas.width : null,
},
container: serializeElement(container),
controls: serializeElement(
container?.querySelector('[data-skin-preview-debug="controls"]') ?? null,
),
nametag: serializeElement(
container?.querySelector('[data-skin-preview-debug="nametag"]') ?? null,
),
subtitle: serializeElement(
container?.querySelector('[data-skin-preview-debug="subtitle"]') ?? null,
),
},
effectiveFit: {
containerSize: fitContainerSize.value,
modelCenter: fitModelCenter.value,
modelOffset: modelOffset.value,
modelSize: fitModelSize.value,
padding: fitResolvedPadding.value,
rotation: fitModelRotation.value,
visibleBounds: fitVisibleBounds.value,
},
fitEnabled: fitEnabled.value,
fitLock: fitLock.value,
lockFitEnabled: lockFitEnabled.value,
meshBounds: getVisibleMeshDebugBounds(scene.value),
overlayCalculations: {
nametagTop: nametagTop.value,
previewControlsPositionStyle: previewControlsPositionStyle.value,
projectedVisibleBounds: projectedVisibleBounds.value,
subtitlePositionStyle: subtitlePositionStyle.value,
},
reasons,
timestamp: new Date().toISOString(),
window: {
devicePixelRatio: window.devicePixelRatio,
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
},
}),
) as SkinPreviewDebugEntry
entries.push(snapshot)
if (entries.length > 100) entries.splice(0, entries.length - 100)
console.log('[SkinPreviewDebug]', snapshot)
}
function queueDebugSnapshot(reason: string) {
if (typeof window === 'undefined') return
pendingDebugReasons.add(reason)
if (debugAnimationFrame !== null) return
debugAnimationFrame = window.requestAnimationFrame(() => {
debugAnimationFrame = null
const reasons = Array.from(pendingDebugReasons)
pendingDebugReasons.clear()
captureDebugSnapshot(reasons)
})
}
function lockFitState() { function lockFitState() {
if (!fitEnabled.value || !lockFitEnabled.value || fitLock.value || !isModelLoaded.value) return if (!fitEnabled.value || !lockFitEnabled.value || fitLock.value || !isModelLoaded.value) return
@@ -304,7 +485,9 @@ export function useSkinPreviewFit({
modelSize: cloneModelTuple(modelSize.value), modelSize: cloneModelTuple(modelSize.value),
padding: { ...resolvedFitPadding.value }, padding: { ...resolvedFitPadding.value },
rotation: modelRotation.value, rotation: modelRotation.value,
visibleBounds: cloneBounds(visibleBounds.value),
} }
queueDebugSnapshot('fit-lock-created')
} }
function resetFitLockForLayoutChange() { function resetFitLockForLayoutChange() {
@@ -312,6 +495,7 @@ export function useSkinPreviewFit({
fitLock.value = null fitLock.value = null
lockFitState() lockFitState()
queueDebugSnapshot('fit-lock-reset')
} }
onMounted(() => { onMounted(() => {
@@ -332,10 +516,12 @@ export function useSkinPreviewFit({
if (didContainerSizeChange) { if (didContainerSizeChange) {
resetFitLockForLayoutChange() resetFitLockForLayoutChange()
queueDebugSnapshot('container-resized')
} }
}) })
resizeObserver.observe(el) resizeObserver.observe(el)
queueDebugSnapshot('mounted')
}) })
watch( watch(
@@ -358,8 +544,22 @@ export function useSkinPreviewFit({
lockFitState() lockFitState()
}) })
watch([modelCenter, modelSize, visibleBounds, resolvedFitPadding], () => {
resetFitLockForLayoutChange()
queueDebugSnapshot('model-inputs-changed')
})
watch(
projectedVisibleBounds,
() => {
queueDebugSnapshot('projection-changed')
},
{ deep: true },
)
onUnmounted(() => { onUnmounted(() => {
resizeObserver?.disconnect() resizeObserver?.disconnect()
if (debugAnimationFrame !== null) window.cancelAnimationFrame(debugAnimationFrame)
}) })
return { return {
@@ -20,8 +20,8 @@ import {
loadTexture as loadSkinTexture, loadTexture as loadSkinTexture,
} from '#ui/utils/webgl/skin-rendering.ts' } from '#ui/utils/webgl/skin-rendering.ts'
import type { SkinPreviewTuple } from './types' import type { SkinPreviewBounds, SkinPreviewTuple } from './types'
import { applyEarsMod, removeEarsMod } from './use-ears-mod-features' import { applyEarsMod, isEarsModFeature, removeEarsMod } from './use-ears-mod-features'
const SKIN_LAYER_DEPTH_BIAS = -1 const SKIN_LAYER_DEPTH_BIAS = -1
@@ -77,16 +77,25 @@ function disposeSceneMaterials(root: THREE.Object3D | null) {
materials.forEach((material) => material.dispose()) materials.forEach((material) => material.dispose())
} }
function getVisibleMeshBox(root: THREE.Object3D): THREE.Box3 | null { function getVisibleMeshBox(
root: THREE.Object3D,
includeMesh: (mesh: THREE.Mesh) => boolean = () => true,
): THREE.Box3 | null {
root.updateWorldMatrix(true, true) root.updateWorldMatrix(true, true)
const result = new THREE.Box3() const result = new THREE.Box3()
const meshBox = new THREE.Box3() const meshBox = new THREE.Box3()
const rootParentInverse = new THREE.Matrix4()
const meshToRootParent = new THREE.Matrix4()
let found = false let found = false
if (root.parent) {
rootParentInverse.copy(root.parent.matrixWorld).invert()
}
root.traverse((object) => { root.traverse((object) => {
const mesh = object as THREE.Mesh const mesh = object as THREE.Mesh
if (!mesh.isMesh || !mesh.geometry || mesh.visible === false) return if (!mesh.isMesh || !mesh.geometry || mesh.visible === false || !includeMesh(mesh)) return
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
if (materials.length && materials.every((material) => material.visible === false)) return if (materials.length && materials.every((material) => material.visible === false)) return
@@ -97,7 +106,8 @@ function getVisibleMeshBox(root: THREE.Object3D): THREE.Box3 | null {
if (!mesh.geometry.boundingBox) return if (!mesh.geometry.boundingBox) return
meshBox.copy(mesh.geometry.boundingBox).applyMatrix4(mesh.matrixWorld) meshToRootParent.multiplyMatrices(rootParentInverse, mesh.matrixWorld)
meshBox.copy(mesh.geometry.boundingBox).applyMatrix4(meshToRootParent)
result.union(meshBox) result.union(meshBox)
found = true found = true
}) })
@@ -105,6 +115,13 @@ function getVisibleMeshBox(root: THREE.Object3D): THREE.Box3 | null {
return found && !result.isEmpty() ? result.clone() : null return found && !result.isEmpty() ? result.clone() : null
} }
function isPlayerMesh(mesh: THREE.Mesh) {
if (isEarsModFeature(mesh)) return false
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
return materials.some((material) => material.name !== 'cape')
}
type MaybeReadonlyRef<T> = Ref<T> | ComputedRef<T> type MaybeReadonlyRef<T> = Ref<T> | ComputedRef<T>
export function useSkinPreviewScene({ export function useSkinPreviewScene({
@@ -136,6 +153,10 @@ export function useSkinPreviewScene({
const transparentTexture = createTransparentTexture() const transparentTexture = createTransparentTexture()
const modelCenter = ref<SkinPreviewTuple>([0, 1, 0]) const modelCenter = ref<SkinPreviewTuple>([0, 1, 0])
const modelSize = ref<SkinPreviewTuple>([1, 2, 1]) const modelSize = ref<SkinPreviewTuple>([1, 2, 1])
const visibleBounds = ref<SkinPreviewBounds>({
min: [-0.5, 0, -0.5],
max: [0.5, 2, 0.5],
})
const isModelLoaded = ref(false) const isModelLoaded = ref(false)
const isTextureLoaded = ref(false) const isTextureLoaded = ref(false)
const hasEarsFeatures = ref(false) const hasEarsFeatures = ref(false)
@@ -252,6 +273,7 @@ export function useSkinPreviewScene({
capeTexture.value = loadedCapeTexture capeTexture.value = loadedCapeTexture
loadedCapeSrc.value = src loadedCapeSrc.value = src
applyCapeTextureToLoadedModel() applyCapeTextureToLoadedModel()
updateModelInfo()
} }
async function loadAndApplyEarsTexture(src: string | undefined) { async function loadAndApplyEarsTexture(src: string | undefined) {
@@ -269,21 +291,44 @@ export function useSkinPreviewScene({
function updateModelInfo() { function updateModelInfo() {
const box = scene.value ? getVisibleMeshBox(scene.value) : null const box = scene.value ? getVisibleMeshBox(scene.value) : null
const playerBox = scene.value ? getVisibleMeshBox(scene.value, isPlayerMesh) : null
if (!box) { if (!box) {
modelCenter.value = [0, 1, 0] modelCenter.value = [0, 1, 0]
modelSize.value = [1, 2, 1] modelSize.value = [1, 2, 1]
visibleBounds.value = {
min: [-0.5, 0, -0.5],
max: [0.5, 2, 0.5],
}
return return
} }
const center = new THREE.Vector3() const center = new THREE.Vector3()
const size = new THREE.Vector3() const playerCenter = new THREE.Vector3()
const rotationCenterBox = playerBox ?? box
box.getCenter(center) box.getCenter(center)
box.getSize(size) rotationCenterBox.getCenter(playerCenter)
modelCenter.value = [center.x, center.y, center.z] const halfWidth = Math.max(
modelSize.value = [Math.max(size.x, 0.001), Math.max(size.y, 0.001), Math.max(size.z, 0.001)] Math.abs(box.min.x - playerCenter.x),
Math.abs(box.max.x - playerCenter.x),
)
const halfDepth = Math.max(
Math.abs(box.min.z - playerCenter.z),
Math.abs(box.max.z - playerCenter.z),
)
modelCenter.value = [playerCenter.x, center.y, playerCenter.z]
modelSize.value = [
Math.max(halfWidth * 2, 0.001),
Math.max(box.max.y - box.min.y, 0.001),
Math.max(halfDepth * 2, 0.001),
]
visibleBounds.value = {
min: [box.min.x, box.min.y, box.min.z],
max: [box.max.x, box.max.y, box.max.z],
}
} }
watch( watch(
@@ -303,6 +348,7 @@ export function useSkinPreviewScene({
texture.value = loadedTexture texture.value = loadedTexture
loadedTextureSrc.value = newSrc loadedTextureSrc.value = newSrc
applyTextureToLoadedModel() applyTextureToLoadedModel()
updateModelInfo()
isTextureLoaded.value = true isTextureLoaded.value = true
}, },
) )
@@ -365,5 +411,6 @@ export function useSkinPreviewScene({
modelCenter, modelCenter,
modelSize, modelSize,
scene, scene,
visibleBounds,
} }
} }
@@ -167,7 +167,7 @@ export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): voi
const propertiesNeedUpdate = setShaderMaterialProperties(mat, { const propertiesNeedUpdate = setShaderMaterialProperties(mat, {
alphaTest: 0.1, alphaTest: 0.1,
flatShading: true, flatShading: true,
side: THREE.FrontSide, side: isSkinLayer ? THREE.DoubleSide : THREE.FrontSide,
toneMapped: false, toneMapped: false,
transparent: isSkinLayer, transparent: isSkinLayer,
}) })