fix&refactor(stage-ui): Shader injection now can correctly deal with MToon material and ShaderMaterial (#542)
* shader fix & refactor
This commit is contained in:
@@ -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=off,2=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 }
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user