mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user