fix(stage-ui-three): cutout bug fix (#1471)
* fix(stage-ui-three): cutout bug fix * fix(stage-ui-three): gemini review comment * fix(stage-ui-three): codex review comment
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import type { VRM } from '@pixiv/three-vrm'
|
||||
import type {
|
||||
Group,
|
||||
Material,
|
||||
Object3D,
|
||||
PerspectiveCamera,
|
||||
ShaderMaterial,
|
||||
@@ -180,6 +181,11 @@ type VrmFrameHook = (vrm: VRM, delta: number) => void
|
||||
const vrmFrameHook = shallowRef<VrmFrameHook>()
|
||||
let disposeBeforeRenderLoop: (() => void | undefined) | undefined
|
||||
|
||||
// material type with optional update function for per-frame update, used for three-vrm's MToon material and custom shader materials with IBL injection
|
||||
type UpdatableMaterial = Material & {
|
||||
update?: (delta: number) => void
|
||||
}
|
||||
|
||||
// Expressions
|
||||
const blink = useBlink()
|
||||
const idleEyeSaccades = useIdleEyeSaccades()
|
||||
@@ -324,6 +330,15 @@ function shouldStashVrmResources(reason: VrmLifecycleReason) {
|
||||
return reason === 'component-unmount'
|
||||
}
|
||||
|
||||
function updateManagedVrmMaterials(activeVrm: VRM | undefined, delta: number) {
|
||||
// NOTICE: three-vrm drives MToon per-frame uniforms, including alphaTest used by MASK cutout,
|
||||
// through material.update(delta). Our render loop updates VRM subsystems manually instead of
|
||||
// calling vrm.update(delta), so material updates must be forwarded here as well.
|
||||
activeVrm?.materials?.forEach((material) => {
|
||||
(material as UpdatableMaterial).update?.(delta)
|
||||
})
|
||||
}
|
||||
|
||||
function bindManagedVrmInstanceRenderLoop() {
|
||||
disposeBeforeRenderLoop?.()
|
||||
|
||||
@@ -335,6 +350,7 @@ function bindManagedVrmInstanceRenderLoop() {
|
||||
vrmAnimationMixer.value?.update(delta)
|
||||
})
|
||||
const activeVrm = vrm.value
|
||||
updateManagedVrmMaterials(activeVrm, delta)
|
||||
const vrmFrameHookMs = measureFrameStep(tracingEnabled, () => {
|
||||
if (activeVrm && vrmFrameHook.value) {
|
||||
try {
|
||||
@@ -686,13 +702,28 @@ async function loadModel() {
|
||||
/*
|
||||
* Shader setting
|
||||
*/
|
||||
// material selection
|
||||
function isMToon(mat: any): boolean {
|
||||
return !!(mat?.isShaderMaterial && mat.userData?.vrmMaterialType === 'MToon'
|
||||
)
|
||||
}
|
||||
const isShaderMat = (m: any): m is ShaderMaterial => !!m?.isShaderMaterial
|
||||
|
||||
function configureInjectedShaderMaterial(mat: ShaderMaterial) {
|
||||
if ('toneMapped' in mat)
|
||||
mat.toneMapped = false
|
||||
if ('envMap' in mat && mat.envMap)
|
||||
mat.envMap = null
|
||||
|
||||
// NPR materials usually use sRGB textures.
|
||||
const tex = (mat as any).map as Texture | undefined
|
||||
if (tex && (tex as any).colorSpace !== undefined) {
|
||||
try {
|
||||
(tex as any).colorSpace = SRGBColorSpace
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Failed to set colorSpace on texture:', e)
|
||||
}
|
||||
}
|
||||
|
||||
injectDiffuseIBL(mat)
|
||||
}
|
||||
|
||||
// MToon material sky box lightProbe setting
|
||||
if (!airiIblProbe && scene.value)
|
||||
airiIblProbe = createIblProbeController(scene.value)
|
||||
@@ -707,31 +738,16 @@ async function loadModel() {
|
||||
mat.envMapIntensity = 1.0
|
||||
mat.needsUpdate = true
|
||||
}
|
||||
else if (isMToon(mat)) {
|
||||
// --- MToon material, add IBL lightProbe only ---
|
||||
// close tone mapping for NPR materials
|
||||
if ('toneMapped' in mat)
|
||||
mat.toneMapped = false
|
||||
else if (mat?.isMToonMaterial) {
|
||||
// --- MToon material ---
|
||||
// MToon is also shader-based and should receive the custom IBL injection.
|
||||
configureInjectedShaderMaterial(mat)
|
||||
}
|
||||
else if (isShaderMat(mat)) {
|
||||
// --- Shader material, further IBL injection needed ---
|
||||
// TODO: stylised shader injection
|
||||
// Lilia: I plan to replace all injected shader code to be my own, so that it can always avoid double injection and unknown user upload VRM injected shader behaviour...
|
||||
if ('toneMapped' in mat)
|
||||
mat.toneMapped = false
|
||||
if ('envMap' in mat && mat.envMap)
|
||||
mat.envMap = null
|
||||
// NPR materials usually use sRGB textures
|
||||
const tex = (mat as any).map as Texture | undefined
|
||||
if (tex && (tex as any).colorSpace !== undefined) {
|
||||
try {
|
||||
(tex as any).colorSpace = SRGBColorSpace
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Failed to set colorSpace on texture:', e)
|
||||
}
|
||||
}
|
||||
injectDiffuseIBL(mat)
|
||||
configureInjectedShaderMaterial(mat)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ uniform vec3 uSHCoeffs[9];
|
||||
varying vec3 vWorldNormal;
|
||||
|
||||
// 3rd-order SH constants
|
||||
const float C0=0.2820947918;
|
||||
const float C0=0.2820947918;
|
||||
const float C1=0.4886025119;
|
||||
const float C2=1.0925484306;
|
||||
const float C2=1.0925484306;
|
||||
const float C3=0.3153915653;
|
||||
const float C4=0.5462742153;
|
||||
|
||||
@@ -62,7 +62,6 @@ export type EnvMode = 'off' | 'skyBox' | 'hemisphere'
|
||||
|
||||
export const isShaderMat = (m: any): m is THREE.ShaderMaterial => !!m?.isShaderMaterial
|
||||
export const isRawShader = (m: any): m is THREE.RawShaderMaterial => !!m?.isRawShaderMaterial
|
||||
export const isMToon = (mat: any) => !!(mat?.isShaderMaterial && mat.userData?.vrmMaterialType === 'MToon')
|
||||
|
||||
export function normalizeEnvMode(v?: string | null): EnvMode {
|
||||
if (v === 'skyBox')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { VRM } from '@pixiv/three-vrm'
|
||||
import type { Group, Object3D } from 'three'
|
||||
import type { Group, Material, Object3D } from 'three'
|
||||
|
||||
import { VRMUtils } from '@pixiv/three-vrm'
|
||||
import { AmbientLight, AnimationMixer, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three'
|
||||
@@ -21,6 +21,20 @@ function disposePreviewRenderer(renderer: WebGLRenderer) {
|
||||
renderer.forceContextLoss()
|
||||
}
|
||||
|
||||
function hasMaterialUpdate(material: Material): material is Material & { update: (delta: number) => void } {
|
||||
return typeof (material as { update?: unknown }).update === 'function'
|
||||
}
|
||||
|
||||
function updatePreviewVrmMaterials(vrm: VRM | undefined, delta: number) {
|
||||
// NOTICE: three-vrm drives MToon per-frame uniforms, including alphaTest used by MASK cutout,
|
||||
// through material.update(delta). The preview path renders a one-shot offscreen frame instead of
|
||||
// using VRM.update(delta), so we need to forward material updates manually before rendering.
|
||||
vrm?.materials?.forEach((material) => {
|
||||
if (hasMaterialUpdate(material))
|
||||
material.update(delta)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a VRM file to an offscreen canvas and return a preview data URL.
|
||||
*/
|
||||
@@ -49,6 +63,7 @@ export async function loadVrmModelPreview(file: File) {
|
||||
let vrmInstance: VRM | undefined
|
||||
let vrmGroup: Group | undefined
|
||||
let mixer: AnimationMixer | undefined
|
||||
const previewDelta = 0.1
|
||||
|
||||
try {
|
||||
const vrmData = await loadVrm(objUrl, { scene, lookAt: true })
|
||||
@@ -71,13 +86,14 @@ export async function loadVrmModelPreview(file: File) {
|
||||
mixer = new AnimationMixer(vrmData._vrm.scene)
|
||||
const action = mixer.clipAction(clip)
|
||||
action.play()
|
||||
mixer.update(0.1)
|
||||
mixer.update(previewDelta)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('Failed to load VRM animation for preview:', err)
|
||||
}
|
||||
|
||||
updatePreviewVrmMaterials(vrmInstance, previewDelta)
|
||||
renderer.render(scene, camera)
|
||||
|
||||
const dataUrl = offscreenCanvas.toDataURL()
|
||||
|
||||
Reference in New Issue
Block a user