feat: add Ears mod support to skin selector (#6775)

* feat: add Ears mod support to skin selector

* fix: lint

* feat: ears mod notice + toggle + fix uv issues

* fix: lint
This commit is contained in:
Calum H.
2026-07-20 18:01:04 +00:00
committed by GitHub
parent ee2ad5f05b
commit 8b6f0ec695
12 changed files with 2119 additions and 45 deletions
+1
View File
@@ -16,6 +16,7 @@ export * from './server-backups-queue'
export * from './server-console'
export * from './server-manage-core-runtime'
export * from './server-permissions'
export { applyEarsMod, removeEarsMod } from './skin-rendering/use-ears-mod-features'
export * from './sticky-observer'
export * from './terminal'
export * from './use-loading-bar-token'
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,7 @@ import {
} from '#ui/utils/webgl/skin-rendering.ts'
import type { SkinPreviewTuple } from './types'
import { applyEarsMod, removeEarsMod } from './use-ears-mod-features'
const SKIN_LAYER_DEPTH_BIAS = -1
@@ -109,13 +110,17 @@ type MaybeReadonlyRef<T> = Ref<T> | ComputedRef<T>
export function useSkinPreviewScene({
selectedModelSrc,
textureSrc,
earsTextureSrc,
capeSrc,
earsEnabled,
initializeAnimations,
cleanupAnimationState,
}: {
selectedModelSrc: MaybeReadonlyRef<string>
textureSrc: MaybeReadonlyRef<string>
earsTextureSrc: MaybeReadonlyRef<string | undefined>
capeSrc: MaybeReadonlyRef<string | undefined>
earsEnabled: MaybeReadonlyRef<boolean>
initializeAnimations: (loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) => void
cleanupAnimationState: (root: THREE.Object3D | null) => void
}) {
@@ -123,16 +128,20 @@ export function useSkinPreviewScene({
const lastCapeSrc = ref<string | undefined>(undefined)
const loadedModelSrc = ref<string | undefined>(undefined)
const loadedTextureSrc = ref<string | undefined>(undefined)
const loadedEarsTextureSrc = ref<string | undefined>(undefined)
const loadedCapeSrc = ref<string | undefined>(undefined)
const texture = shallowRef<THREE.Texture | null>(null)
const earsTexture = shallowRef<THREE.Texture | null>(null)
const capeTexture = shallowRef<THREE.Texture | null>(null)
const transparentTexture = createTransparentTexture()
const modelCenter = ref<SkinPreviewTuple>([0, 1, 0])
const modelSize = ref<SkinPreviewTuple>([1, 2, 1])
const isModelLoaded = ref(false)
const isTextureLoaded = ref(false)
const hasEarsFeatures = ref(false)
let modelLoadVersion = 0
let textureLoadVersion = 0
let earsTextureLoadVersion = 0
let capeLoadVersion = 0
let isUnmounted = false
@@ -147,6 +156,18 @@ export function useSkinPreviewScene({
}
applyTexture(scene.value, texture.value)
const featureTextureSrc = earsTextureSrc.value
const featureTexture = featureTextureSrc ? earsTexture.value : texture.value
if (
!featureTexture ||
(featureTextureSrc && loadedEarsTextureSrc.value !== featureTextureSrc)
) {
removeEarsMod(scene.value)
hasEarsFeatures.value = false
return
}
hasEarsFeatures.value = applyEarsMod(scene.value, featureTexture, earsEnabled.value)
}
function applyCapeTextureToLoadedModel() {
@@ -173,6 +194,7 @@ export function useSkinPreviewScene({
const previousScene = scene.value
cleanupAnimationState(previousScene)
removeEarsMod(previousScene)
disposeSceneMaterials(previousScene)
scene.value = clonedScene
loadedModelSrc.value = src
@@ -232,6 +254,19 @@ export function useSkinPreviewScene({
applyCapeTextureToLoadedModel()
}
async function loadAndApplyEarsTexture(src: string | undefined) {
const loadVersion = ++earsTextureLoadVersion
hasEarsFeatures.value = false
const loadedEarsTexture = src ? await loadAndApplyTexture(src) : null
if (isUnmounted || loadVersion !== earsTextureLoadVersion) return
earsTexture.value = loadedEarsTexture
loadedEarsTextureSrc.value = src
applyTextureToLoadedModel()
updateModelInfo()
}
function updateModelInfo() {
const box = scene.value ? getVisibleMeshBox(scene.value) : null
@@ -261,6 +296,7 @@ export function useSkinPreviewScene({
const loadVersion = ++textureLoadVersion
isTextureLoaded.value = false
hasEarsFeatures.value = false
const loadedTexture = await loadAndApplyTexture(newSrc)
if (isUnmounted || loadVersion !== textureLoadVersion) return
@@ -270,6 +306,19 @@ export function useSkinPreviewScene({
isTextureLoaded.value = true
},
)
watch(
() => earsTextureSrc.value,
async (newEarsTextureSrc) => {
await loadAndApplyEarsTexture(newEarsTextureSrc)
},
)
watch(
() => earsEnabled.value,
() => {
applyTextureToLoadedModel()
updateModelInfo()
},
)
watch(
() => capeSrc.value,
async (newCapeSrc) => {
@@ -284,6 +333,7 @@ export function useSkinPreviewScene({
loadedTextureSrc.value = textureSrc.value
isTextureLoaded.value = true
await loadAndApplyEarsTexture(earsTextureSrc.value)
await loadModel(selectedModelSrc.value)
if (capeSrc.value) {
@@ -298,15 +348,18 @@ export function useSkinPreviewScene({
isUnmounted = true
modelLoadVersion++
textureLoadVersion++
earsTextureLoadVersion++
capeLoadVersion++
cleanupAnimationState(scene.value)
removeEarsMod(scene.value)
disposeSceneMaterials(scene.value)
scene.value = null
transparentTexture.dispose()
})
return {
hasEarsFeatures,
isModelLoaded,
isTextureLoaded,
modelCenter,