fix&refactor(stage-ui): Shader injection now can correctly deal with MToon material and ShaderMaterial (#542)

* shader fix & refactor
This commit is contained in:
Lilia_Chen
2025-09-02 00:25:54 +01:00
committed by GitHub
parent 0deb808afc
commit 6b556019e2
5 changed files with 229 additions and 341 deletions
@@ -43,7 +43,6 @@ const {
envSelect,
skyBoxIntensity,
specularMix,
} = storeToRefs(vrm)
const trackingOptions = computed(() => [
{ value: 'camera', label: t('settings.vrm.scale-and-position.eye-tracking-mode.options.option.camera'), class: 'col-start-3' },
@@ -202,14 +201,9 @@ const tabList = [
<div grid="~ cols-5 gap-1" p-2>
<PropertyNumber
v-model="skyBoxIntensity"
:config="{ min: 0, max: 2, step: 0.01, label: 'Intensity' }"
:config="{ min: 0, max: 1, step: 0.01, label: 'Intensity' }"
:label="t('settings.vrm.skybox.skybox-intensity')"
/>
<PropertyNumber
v-model="specularMix"
:config="{ min: 0, max: 1, step: 0.01, label: 'Mix' }"
:label="t('settings.vrm.skybox.skybox-specular-mix')"
/>
</div>
</div>
</template>
@@ -1,7 +1,7 @@
<script setup lang="ts">
/* eslint-disable style/max-statements-per-line */
import type { VRMCore } from '@pixiv/three-vrm-core'
import type { AnimationClip, Group, Material, SphericalHarmonics3, Texture } from 'three'
import type { AnimationClip, Group, SphericalHarmonics3, Texture } from 'three'
import type * as THREE from 'three'
import { VRMUtils } from '@pixiv/three-vrm'
import { useLoop, useTresContext } from '@tresjs/core'
@@ -11,13 +11,9 @@ import {
AnimationMixer,
MathUtils,
Mesh,
MeshBasicMaterial,
MeshPhysicalMaterial,
MeshStandardMaterial,
MeshToonMaterial,
Quaternion,
RawShaderMaterial,
ShaderMaterial,
SRGBColorSpace,
Vector3,
VectorKeyframeTrack,
@@ -27,6 +23,12 @@ import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
import { clipFromVRMAnimation, loadVRMAnimation, useBlink, useIdleEyeSaccades } from '../../../composables/vrm/animation'
import { loadVrm } from '../../../composables/vrm/core'
import { useVRMEmote } from '../../../composables/vrm/expression'
import {
createIblProbeController,
injectDiffuseIBL,
normalizeEnvMode,
updateNprShaderSetting,
} from '../../../composables/vrm/shader/ibl'
import { useVRM } from '../../../stores/vrm'
const props = defineProps<{
@@ -69,7 +71,6 @@ const {
trackingMode,
envSelect,
specularMix,
skyBoxIntensity,
} = storeToRefs(vrmStore)
const vrmGroup = ref<Group>()
@@ -77,52 +78,8 @@ const idleEyeSaccades = useIdleEyeSaccades()
const nprProgramVersion = ref(0)
// Try the best to extract the matcap texture from various possible places
// TODO: this function should be refactored and moved to composables/vrm
function extractMatcapTexture(mat: any): Texture | null {
// a) MeshMatcapMaterial
if ('matcap' in mat && mat.matcap)
return mat.matcap as Texture
// b) For common MToon materials, it might be under uniforms
const u = (mat as any).uniforms
if (u) {
// try different possible names...
if (u.matcapTexture?.value)
return u.matcapTexture.value as Texture
if (u.sphereAddTexture?.value)
return u.sphereAddTexture.value as Texture
if (u._MatCapTex?.value)
return u._MatCapTex.value as Texture
if (u._SphereAdd?.value)
return u._SphereAdd.value as Texture
}
// c) If the matcap is in the gltf extension
const ud = (mat as any).userData || {}
const ext = ud.gltfExtensions?.VRMC_materials_mtoon || ud.vrmMaterialProperties || ud.mtoon
if (ext) {
// Different possible names...
const cand
= ext.matcapTexture
|| ext.sphereAddTexture
|| ext.matcap
|| ext.sphereAdd
if (cand && cand.isTexture)
return cand as Texture
}
return null
}
// flaten the irrSH to array of Vector3
function shToVec3Array(sh: SphericalHarmonics3 | null): Vector3[] {
const arr: Vector3[] = Array.from({ length: 9 }, () => new Vector3())
if (!sh)
return arr
for (let i = 0; i < 9; i++) arr[i].copy(sh.coefficients[i])
return arr
}
// For MToon IBL
let airiIblProbe: ReturnType<typeof createIblProbeController> | null = null
async function loadModel() {
await until(modelLoading).not.toBeTruthy()
@@ -257,264 +214,58 @@ async function loadModel() {
vrmEmote.value = useVRMEmote(_vrm)
// TODO: perhaps we should allow user to choose whether to enable
// this kind of behavior or not?
// - Lilia: NO need, cause NPR mat should always follow NPR route, and BPR mat shoule always follow PBR route.
// WORKAROUND: set to use envMapIntensity for all matched materials
// - Lilia: YES, envMapIntensity will be exposed to GUI user to adjust.
// REVIEW: MeshToonMaterial, and MeshBasicMaterial will not be affected
// since they do not have envMapIntensity property
// - Lilia: NPR mat can be affected by envMapIntensity if we choose to user skybox as envMap
// This part will be implemented as a NPR HDRI shader injection
// material selection
function isMToon(mat: any): boolean {
return !!(mat?.isShaderMaterial && mat.userData?.vrmMaterialType === 'MToon'
)
}
const isShaderMat = (m: any): m is THREE.ShaderMaterial => !!m?.isShaderMaterial
// refactoring
// MToon material skybox lightProbe setting
if (!airiIblProbe && scene.value)
airiIblProbe = createIblProbeController(scene.value)
// Material traverse setting
_vrm.scene.traverse((child) => {
if (child instanceof Mesh && child.material) {
const material = Array.isArray(child.material) ? child.material : [child.material]
material.forEach((mat) => {
// console.debug("shader material: ", mat)
if (mat instanceof MeshStandardMaterial || mat instanceof MeshPhysicalMaterial) {
// Should read envMap intensity from outside props
mat.envMapIntensity = 1.0
mat.needsUpdate = true
}
else if (
mat instanceof MeshToonMaterial
|| mat instanceof MeshBasicMaterial
|| mat instanceof ShaderMaterial
|| mat instanceof RawShaderMaterial
) {
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 (isShaderMat(mat)) {
// --- Shader material, further IBL injection needed ---
// disable envMap for NPR materials, envMap will be reassigned by default skybox
// close tone mapping for NPR materials
if ('toneMapped' in mat)
mat.toneMapped = false
if ('envMap' in mat && mat.envMap)
mat.envMap = null
// NPR materials usually use sRGB textures
if ('map' in mat && mat.map && 'colorSpace' in mat.map) {
// try...catch to avoid some weird error (diff verion of three.js?)
try { mat.map.colorSpace = SRGBColorSpace }
catch {}
const tex = (mat as any).map as THREE.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)
}
}
// Foce recompile shader to inject npr env shader at the begining
const baseKey = mat.customProgramCacheKey?.() ?? ''
mat.customProgramCacheKey = () => `${baseKey}|npr:${nprProgramVersion.value}`
// NPR shader injection
const prevOnBeforeCompile = mat.onBeforeCompile
mat.onBeforeCompile = (shader: any, renderer: any) => {
// Keep the previous onBeforeCompile behaviour if any
prevOnBeforeCompile?.(shader, renderer)
// Obtain skybox texture from scene environment
const equirectTex = props.nprEquirectTex ?? null
// use the type assertion to fix TS error...
const m = mat as unknown as Material & {
extensions?: { shaderTextureLOD?: boolean }
}
m.extensions = { ...(m.extensions || {}), shaderTextureLOD: true }
// If matcap texture is available, then use matcap in IBL specular reflection
const matcapTex = extractMatcapTexture(mat)
// If no normal, then skip the rest
const hasNormal = shader.fragmentShader.includes('vNormal')
if (!hasNormal)
return
// --- vWorldPos/vWorldNormal ---
if (!shader.vertexShader.includes('varying vec3 vWorldPos')) {
shader.vertexShader
= `
varying vec3 vWorldPos;
varying vec3 vWorldNormal;
${shader.vertexShader}`
}
if (!shader.fragmentShader.includes('varying vec3 vWorldPos')) {
shader.fragmentShader
= `
varying vec3 vWorldPos;
varying vec3 vWorldNormal;
${shader.fragmentShader}`
}
// --- vertex shader injection ---
shader.vertexShader = shader.vertexShader
.replace(
'#include <defaultnormal_vertex>',
`
#include <defaultnormal_vertex>
vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );
`,
)
.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
vWorldPos = ( modelMatrix * vec4( transformed, 1.0 ) ).xyz;
`,
)
// Setup uniforms
shader.uniforms.uNprEnvMode = { value: (envSelect.value === 'hemisphere') ? 0 : 2 } // 0=hemi: no further injection; 2=skybox
shader.uniforms.uEnvIntensity = { value: skyBoxIntensity.value } // exposed to GUI
shader.uniforms.uEnvMapEquirect = { value: equirectTex }
shader.uniforms.uSpecularMix = { value: specularMix.value }
// LOD mip
shader.uniforms.uEnvMaxMip = { value: 8.0 }
shader.uniforms.uBrightMip = { value: 2.0 } // bright color = low mip
shader.uniforms.uShadowMip = { value: 8.0 } // shadow color = high mip
// Tint
// 染色强度控制
shader.uniforms.uHighlightTint = { value: 0.6 }
shader.uniforms.uShadowTint = { value: 0.35 }
// Specular uniforms for NPR skybox
shader.uniforms.uSpecToonThreshold = { value: 0.9 } // highlight threshold
shader.uniforms.uSpecToonWidth = { value: 0.15 } // highlight width
shader.uniforms.uSpecPower = { value: 10.0 } // highlight sharpness
shader.uniforms.uSpecMip = { value: 8.0 } // default LOD mip level for specular
// Irradiance sampling
const emptySH: Vector3[] = Array.from({ length: 9 }, () => new Vector3())
shader.uniforms.uSHCoeffs = { value: emptySH }
// Specular matcap
// shader.uniforms.uUseMatcap = { value: !!matcapTex }
shader.uniforms.uUseMatcap = { value: false }
shader.uniforms.uMatcap = { value: matcapTex ?? null }
shader.uniforms.uMatcapIntensity = { value: 1.0 }
// console.debug('same matcap?', matcapTex === shader.uniforms.uMatcap.value)
// Shader injection!
shader.fragmentShader = shader.fragmentShader.replace(
'#include <common>',
`
#include <common>
uniform int uNprEnvMode; // 0=off, 2=Skybox
uniform float uEnvIntensity;
uniform float uSpecularMix; // 0=diffuse only, 1=specular only
uniform float uSpecToonThreshold;
uniform float uSpecToonWidth;
uniform float uSpecPower;
uniform bool uUseMatcap;
uniform sampler2D uMatcap;
uniform float uMatcapIntensity;
uniform sampler2D uEnvMapEquirect; // Skybox(equirect)
uniform vec3 uSHCoeffs[9]; // for irradiance
uniform float uSpecMip; // default LOD mip for specular
// --- Direction to equirectangular UV ---
vec2 dirToEquirectUV(vec3 d){
d = normalize(d);
float phi = atan(d.z, d.x);
float th = asin(clamp(d.y, -1.0, 1.0));
return vec2(0.5 + phi/(2.0*PI), 0.5 - th/PI);
}
// --- Spherical Harmonics (3rd order) for diffuse IBL ---
// Constants for SH basis functions
const float C0 = 1.0 / (2.0 * sqrt(PI));
const float C1 = sqrt(3.0 / PI) / 2.0;
const float C2 = sqrt(15.0 / PI) / 2.0;
const float C3 = sqrt(5.0 / PI) / 4.0;
const float C4 = sqrt(15.0 / PI) / 4.0;
vec3 evalIrradianceSH( vec3 n ) {
vec3 sh = vec3(0.0);
sh += uSHCoeffs[0] * C0;
sh += uSHCoeffs[1] * (-C1 * n.y);
sh += uSHCoeffs[2] * ( C1 * n.z);
sh += uSHCoeffs[3] * (-C1 * n.x);
sh += uSHCoeffs[4] * ( C2 * n.x * n.y);
sh += uSHCoeffs[5] * (-C2 * n.y * n.z);
sh += uSHCoeffs[6] * ( C3 * (3.0 * n.z * n.z - 1.0));
sh += uSHCoeffs[7] * (-C2 * n.x * n.z);
sh += uSHCoeffs[8] * ( C4 * (n.x * n.x - n.y * n.y));
return sh;
}
`,
).replace(
'#include <dithering_fragment>',
`
// --- NPR skybox env lighting injection ---
vec3 n = normalize(vNormal);
vec3 nW = inverseTransformDirection(n, viewMatrix);
vec3 envCol = vec3(0.0);
if(uNprEnvMode == 2) {
// View direction in world space
#ifdef USE_VIEWPOSITION
vec3 v = normalize(-vViewPosition);
vec3 vW = inverseTransformDirection(v, viewMatrix);
#else
vec3 vW = normalize(-cameraPosition);
#endif
// Reflection direction in world space
vec3 rW = reflect(-vW, nW);
// To resolve the upside-down reflection issue of equirect map
// nW.y = -nW.y;
// rW.y = -rW.y;
// --- IBL Diffusion ---
// SH-based irradiance
vec3 I = evalIrradianceSH(nW);
vec3 albedo = gl_FragColor.rgb;
// albedo/pi * I(n)
vec3 envDiff = (albedo / 3.14159265) * I * (uEnvIntensity);
// TODO: Tint
// --- IBL Specular reflection ---
// TODO: a more stylistic specular reflection model. Is specular necessary?
vec3 envSpec;
if (uUseMatcap) {
// Matcap-based specular
vec3 V = vec3(0.0, 0.0, 1.0);
vec3 nV = n;
vec3 R = reflect(-V, nV);
float m = 2.0 * sqrt( pow(R.x, 2.0) + pow(R.y, 2.0) + pow(R.z + 1.0, 2.0) );
vec2 uvMC = R.xy / m * 0.5 + 0.5;
vec3 matcapCol = texture2D(uMatcap, uvMC).rgb;
envSpec = matcapCol * uMatcapIntensity;
}
else {
// Equirect-based specular, LOD needed for NPR
vec3 N = normalize(vWorldNormal);
vec3 V = normalize(cameraPosition - vWorldPos); // camera to frag
vec3 R = reflect(-V, N);
vec2 uvRef = dirToEquirectUV(R);
#if __VERSION__ >= 300
vec3 envRef = textureLod(uEnvMapEquirect, uvRef, uSpecMip).rgb;
#else
#ifdef GL_EXT_shader_texture_lod
vec3 envRef = texture2DLodEXT(uEnvMapEquirect, uvRef, uSpecMip).rgb;
#else
vec3 envRef = texture2D(uEnvMapEquirect, uvRef).rgb;
#endif
#endif
// Blinn/Phong
float specRaw = clamp(dot(R, V), 0.0, 1.0);
float sToon = smoothstep(uSpecToonThreshold - uSpecToonWidth,
uSpecToonThreshold + uSpecToonWidth, specRaw);
envSpec = pow(sToon, uSpecPower) * envRef * uEnvIntensity;
}
// Mix specular and diffuse
envCol = mix(envDiff, envSpec, uSpecularMix);
// skybox color mixing
gl_FragColor.rgb += envCol;
}
// --- Injection ends ---
#include <dithering_fragment>
`,
)
mat.userData.__nprUniforms = shader.uniforms
}
mat.needsUpdate = true
// refactoring, IBL injection
// TODO: it should be an unified shader injection entrance
injectDiffuseIBL(mat)
}
// console.debug('material: ', mat)
})
}
})
@@ -594,59 +345,30 @@ function componentCleanUp() {
if (vrm.value) {
vrm.value.scene.removeFromParent()
VRMUtils.deepDispose(vrm.value.scene)
airiIblProbe?.dispose()
}
}
// Switch to NPR SkyBox
function updateNprUniforms(tex: Texture | null, nprIrrSH?: SphericalHarmonics3 | null) {
const root = vrm.value?.scene
if (!root)
return
const mode = (envSelect.value === 'skyBox' && !!tex) ? 2 : 0 // 0=off2=skybox
root.traverse((child) => {
if (child instanceof Mesh && child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material]
mats.forEach((mat) => {
const u = mat.userData?.__nprUniforms
if (!u)
return
// Skybox texture and mode
u.uEnvMapEquirect.value = tex
u.uNprEnvMode.value = mode
u.uEnvIntensity.value = skyBoxIntensity.value
u.uSpecularMix.value = specularMix.value
// Update SH coeffs after the skybox is ready
if (u.uSHCoeffs && nprIrrSH) {
// console.debug('Updating SH coeffs with new props.nprIrrSH:', props.nprIrrSH)
const irrSH = shToVec3Array(nprIrrSH)
for (let i = 0; i < 9; i++) {
u.uSHCoeffs.value[i].copy(irrSH[i])
}
// console.debug('Updated SH coeffs:', u.uSHCoeffs.value)
}
// Update LOD max mip based on the texture size when the skybox is ready
if (tex?.image?.width && tex?.image?.height) {
const maxMip = Math.floor(Math.log2(Math.max(tex.image.width, tex.image.height)))
u.uEnvMaxMip.value = maxMip
u.uShadowMip.value = maxMip // max mip for shadow tint
}
})
}
})
}
// watch NPR skybox
watch(
() => [
envSelect.value,
props.nprEquirectTex,
skyBoxIntensity.value,
specularMix.value,
props.nprIrrSH,
],
async () => {
if (!vrm.value)
return
nprProgramVersion.value += 1
updateNprUniforms(props.nprEquirectTex ?? null, props.nprIrrSH ?? null)
// updateNprUniforms(props.nprEquirectTex ?? null, props.nprIrrSH ?? null)
const mode = normalizeEnvMode(envSelect.value)
updateNprShaderSetting(vrm.value?.scene, {
mode,
intensity: skyBoxIntensity.value,
sh: props.nprIrrSH ?? null,
})
airiIblProbe?.update(mode, skyBoxIntensity.value, props.nprIrrSH ?? null)
},
{ immediate: true, deep: false },
)
@@ -2,3 +2,4 @@ export * from './animation'
export * from './core'
export * from './expression'
export * from './loader'
export * from './shader/ibl'
@@ -0,0 +1,173 @@
// stage-ui/composables/shader/ibl.ts
import * as THREE from 'three'
// ===== head guard shader injection =====
const VS_DECL = `
#ifndef AIRI_DIFFUSE_VS_DECL
#define AIRI_DIFFUSE_VS_DECL
varying vec3 vWorldNormal;
#endif
`
const VS_APPLY = `
#ifndef AIRI_DIFFUSE_VS_APPLY
#define AIRI_DIFFUSE_VS_APPLY
vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );
#endif
`
const FS_COMMON = `
#ifndef AIRI_DIFFUSE_COMMON
#define AIRI_DIFFUSE_COMMON
uniform int uNprEnvMode; // 0=off, 2=skybox
uniform float uEnvIntensity;
uniform vec3 uSHCoeffs[9];
varying vec3 vWorldNormal;
// 3rd-order SH constants
const float C0=0.2820947918;
const float C1=0.4886025119;
const float C2=1.0925484306;
const float C3=0.3153915653;
const float C4=0.5462742153;
vec3 AIRI_evalIrradianceSH(vec3 n){
n = normalize(n);
vec3 r = uSHCoeffs[0]*C0;
r += uSHCoeffs[1]*(-C1*n.y);
r += uSHCoeffs[2]*( C1*n.z);
r += uSHCoeffs[3]*(-C1*n.x);
r += uSHCoeffs[4]*( C2*n.x*n.y);
r += uSHCoeffs[5]*(-C2*n.y*n.z);
r += uSHCoeffs[6]*( C3*(3.0*n.z*n.z-1.0));
r += uSHCoeffs[7]*(-C2*n.x*n.z);
r += uSHCoeffs[8]*( C4*(n.x*n.x-n.y*n.y));
return r;
}
#endif
`
const FS_APPLY = `
#ifndef AIRI_DIFFUSE_APPLY
#define AIRI_DIFFUSE_APPLY
if (uNprEnvMode == 2) {
vec3 I = AIRI_evalIrradianceSH(normalize(vWorldNormal));
gl_FragColor.rgb += (gl_FragColor.rgb / PI) * I * uEnvIntensity;
}
#endif
`
// ===== Utility tools =====
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')
return 'skyBox'
if (v === 'hemisphere')
return 'hemisphere'
return 'off'
}
// Turn SphericalHarmonics3 to vec3[9] uniforms
function assignSHUniform(u: any, sh: THREE.SphericalHarmonics3 | null | undefined) {
if (!u?.uSHCoeffs || !u.uSHCoeffs.value || !Array.isArray(u.uSHCoeffs.value))
return
if (!sh)
return
for (let i = 0; i < 9; i++) {
u.uSHCoeffs.value[i] ||= new THREE.Vector3()
u.uSHCoeffs.value[i].copy(sh.coefficients[i])
}
}
// ===== Shader Material: IBL shader injection =====
export function injectDiffuseIBL(mat: THREE.ShaderMaterial) {
const baseKey = mat.customProgramCacheKey?.() ?? ''
mat.customProgramCacheKey = () => `${baseKey}|airi-diffuse-ibl`
const prev = mat.onBeforeCompile
mat.onBeforeCompile = (shader: any, renderer: any) => {
prev?.(shader, renderer)
// vertex shader declare & apply
if (!shader.vertexShader.includes('AIRI_DIFFUSE_VS_DECL')) {
shader.vertexShader = `${VS_DECL}\n${shader.vertexShader}`
}
if (shader.vertexShader.includes('#include <defaultnormal_vertex>')
&& !shader.vertexShader.includes('AIRI_DIFFUSE_VS_APPLY')) {
shader.vertexShader = shader.vertexShader.replace(
'#include <defaultnormal_vertex>',
`#include <defaultnormal_vertex>\n${VS_APPLY}`,
)
}
// fragement shader common
if (!shader.fragmentShader.includes('AIRI_DIFFUSE_COMMON')) {
shader.fragmentShader = shader.fragmentShader.replace(
'#include <common>',
`#include <common>\n${FS_COMMON}`,
)
}
// fragement shader apply
if (!shader.fragmentShader.includes('AIRI_DIFFUSE_APPLY')) {
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
`${FS_APPLY}\n#include <dithering_fragment>`,
)
}
// uniforms
const emptySH = Array.from({ length: 9 }, () => new THREE.Vector3())
shader.uniforms.uNprEnvMode ||= { value: 0 }
shader.uniforms.uEnvIntensity ||= { value: 0.0 }
shader.uniforms.uSHCoeffs ||= { value: emptySH };
(mat.userData ||= {}).__airiIbl = shader.uniforms
}
if ('toneMapped' in mat)
(mat as any).toneMapped = false
mat.needsUpdate = true
}
// update shader settings
export function updateNprShaderSetting(
root: THREE.Object3D,
opts: { mode: EnvMode, intensity: number, sh?: THREE.SphericalHarmonics3 | null },
) {
const shaderMode = opts.mode === 'skyBox' ? 2 : 0
root.traverse((o) => {
const mesh = o as THREE.Mesh
const raw = (mesh as any).material
const mats: any[] = raw ? (Array.isArray(raw) ? raw : [raw]) : []
mats.forEach((m) => {
const u = m?.userData?.__airiIbl
if (!u)
return
u.uNprEnvMode.value = shaderMode
u.uEnvIntensity.value = opts.intensity
assignSHUniform(u, opts.sh ?? null)
})
})
}
// ===== MToon LightProbe IBL =====
export function createIblProbeController(scene: THREE.Scene) {
const probe = new THREE.LightProbe()
probe.name = 'AIRI_IBL_Probe'
scene.add(probe)
function update(mode: EnvMode, intensity: number, sh?: THREE.SphericalHarmonics3 | null) {
probe.intensity = (mode === 'skyBox') ? intensity : 0
if (sh)
probe.sh.copy(sh)
}
function dispose() {
probe.parent?.remove(probe)
}
return { update, dispose }
}
-2
View File
@@ -82,7 +82,6 @@ export const useVRM = defineStore('vrm', () => {
// environment related setting
const envSelect = useLocalStorage('settings/vrm/envEnabled', 'skyBox' as 'hemisphere' | 'skyBox')
const skyBoxSrc = useLocalStorage('settings/vrm/skyBoxUrl', defaultSkyBoxSrc)
const specularMix = useLocalStorage('settings/vrm/specularMix', 0)
const skyBoxIntensity = useLocalStorage('settings/vrm/skyBoxIntensity', 0.1)
return {
@@ -116,7 +115,6 @@ export const useVRM = defineStore('vrm', () => {
eyeHeight,
envSelect,
skyBoxSrc,
specularMix,
skyBoxIntensity,
shouldUpdateView,