@aether-stack-dev/client-sdk 1.2.5 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/dist/__tests__/mocks/EXRLoader.d.ts +3 -0
- package/dist/__tests__/mocks/HDRLoader.d.ts +3 -0
- package/dist/__tests__/mocks/KTX2Loader.d.ts +5 -0
- package/dist/__tests__/mocks/isows.d.ts +19 -0
- package/dist/__tests__/mocks/react-jsx-runtime.d.ts +9 -0
- package/dist/__tests__/mocks/react.d.ts +18 -0
- package/dist/__tests__/setup.d.ts +8 -8
- package/dist/avatar/VRMAvatar.d.ts +34 -11
- package/dist/avatar/compatibility.d.ts +12 -0
- package/dist/avatar/environment.d.ts +6 -0
- package/dist/avatar/index.d.ts +1 -1
- package/dist/avatar/postProcessing.d.ts +20 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react.esm.js +710 -69
- package/dist/react.js +710 -68
- package/package.json +26 -16
- package/dist/AnalyticsCollector.d.ts +0 -62
- package/dist/BillingMonitor.d.ts +0 -36
- package/dist/ConnectionStateManager.d.ts +0 -49
- package/dist/MediaManager.d.ts +0 -21
- package/dist/PerformanceMonitor.d.ts +0 -33
- package/dist/SecurityLogger.d.ts +0 -26
- package/dist/SessionManager.d.ts +0 -20
- package/dist/UsageTracker.d.ts +0 -21
package/dist/react.js
CHANGED
|
@@ -7,6 +7,16 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
7
7
|
var THREE = require('three');
|
|
8
8
|
var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
|
|
9
9
|
var threeVrm = require('@pixiv/three-vrm');
|
|
10
|
+
var threeVrmAnimation = require('@pixiv/three-vrm-animation');
|
|
11
|
+
var EffectComposer_js = require('three/examples/jsm/postprocessing/EffectComposer.js');
|
|
12
|
+
var RenderPass_js = require('three/examples/jsm/postprocessing/RenderPass.js');
|
|
13
|
+
var UnrealBloomPass_js = require('three/examples/jsm/postprocessing/UnrealBloomPass.js');
|
|
14
|
+
var SSAOPass_js = require('three/examples/jsm/postprocessing/SSAOPass.js');
|
|
15
|
+
var BokehPass_js = require('three/examples/jsm/postprocessing/BokehPass.js');
|
|
16
|
+
var OutputPass_js = require('three/examples/jsm/postprocessing/OutputPass.js');
|
|
17
|
+
var HDRLoader_js = require('three/examples/jsm/loaders/HDRLoader.js');
|
|
18
|
+
var EXRLoader_js = require('three/examples/jsm/loaders/EXRLoader.js');
|
|
19
|
+
var KTX2Loader_js = require('three/examples/jsm/loaders/KTX2Loader.js');
|
|
10
20
|
|
|
11
21
|
function _interopNamespaceDefault(e) {
|
|
12
22
|
var n = Object.create(null);
|
|
@@ -49,8 +59,18 @@ const CRITICAL_BLENDSHAPES = [
|
|
|
49
59
|
'jawOpen', 'eyeBlinkLeft', 'eyeBlinkRight',
|
|
50
60
|
];
|
|
51
61
|
const DEFAULT_BLENDSHAPE_MAP = Object.fromEntries(ARKIT_BLENDSHAPES.map(name => [name, name]));
|
|
52
|
-
|
|
53
|
-
...DEFAULT_BLENDSHAPE_MAP
|
|
62
|
+
const VROID_BLENDSHAPE_MAP = {
|
|
63
|
+
...DEFAULT_BLENDSHAPE_MAP,
|
|
64
|
+
jawOpen: 'Fcl_MTH_A',
|
|
65
|
+
mouthFunnel: 'Fcl_MTH_U',
|
|
66
|
+
mouthSmileLeft: 'Fcl_MTH_Fun',
|
|
67
|
+
mouthSmileRight: 'Fcl_MTH_Fun',
|
|
68
|
+
eyeBlinkLeft: 'Fcl_EYE_Close_L',
|
|
69
|
+
eyeBlinkRight: 'Fcl_EYE_Close_R',
|
|
70
|
+
browDownLeft: 'Fcl_BRW_Angry_L',
|
|
71
|
+
browDownRight: 'Fcl_BRW_Angry_R',
|
|
72
|
+
browInnerUp: 'Fcl_BRW_Surprised',
|
|
73
|
+
};
|
|
54
74
|
|
|
55
75
|
const IDX_JAW_OPEN = ARKIT_BLENDSHAPES.indexOf('jawOpen');
|
|
56
76
|
const IDX_MOUTH_FUNNEL = ARKIT_BLENDSHAPES.indexOf('mouthFunnel');
|
|
@@ -818,7 +838,6 @@ function useAStackCSR(options) {
|
|
|
818
838
|
};
|
|
819
839
|
}
|
|
820
840
|
|
|
821
|
-
const DEFAULT_MAX_MODEL_SIZE = 30 * 1024 * 1024; // 30MB
|
|
822
841
|
const HIGH_VERTEX_THRESHOLD = 100000;
|
|
823
842
|
function buildCompatibilityReport(vrm) {
|
|
824
843
|
const foundNames = new Set();
|
|
@@ -855,7 +874,9 @@ function buildCompatibilityReport(vrm) {
|
|
|
855
874
|
const supported = [];
|
|
856
875
|
const missing = [];
|
|
857
876
|
for (const name of ARKIT_BLENDSHAPES) {
|
|
858
|
-
|
|
877
|
+
const vroidMapped = VROID_BLENDSHAPE_MAP[name];
|
|
878
|
+
if (foundNames.has(name) || foundNames.has(name.toLowerCase())
|
|
879
|
+
|| (vroidMapped && (foundNames.has(vroidMapped) || foundNames.has(vroidMapped.toLowerCase())))) {
|
|
859
880
|
supported.push(name);
|
|
860
881
|
}
|
|
861
882
|
else {
|
|
@@ -878,45 +899,205 @@ function buildCompatibilityReport(vrm) {
|
|
|
878
899
|
modelStats: { vertexCount, textureCount, morphTargetCount },
|
|
879
900
|
};
|
|
880
901
|
}
|
|
881
|
-
|
|
902
|
+
|
|
903
|
+
function createPostProcessing(renderer, scene, camera, config, width, height) {
|
|
904
|
+
const composer = new EffectComposer_js.EffectComposer(renderer);
|
|
905
|
+
composer.addPass(new RenderPass_js.RenderPass(scene, camera));
|
|
906
|
+
if (config.bloom !== undefined && config.bloom > 0) {
|
|
907
|
+
composer.addPass(new UnrealBloomPass_js.UnrealBloomPass(new THREE__namespace.Vector2(width, height), config.bloom * 0.8, 0.4, 0.85));
|
|
908
|
+
}
|
|
909
|
+
if (config.ao !== undefined && config.ao > 0) {
|
|
910
|
+
const ssao = new SSAOPass_js.SSAOPass(scene, camera, width, height);
|
|
911
|
+
ssao.kernelRadius = 8 * config.ao;
|
|
912
|
+
ssao.minDistance = 0.005;
|
|
913
|
+
ssao.maxDistance = 0.1;
|
|
914
|
+
composer.addPass(ssao);
|
|
915
|
+
}
|
|
916
|
+
if (config.dof) {
|
|
917
|
+
composer.addPass(new BokehPass_js.BokehPass(scene, camera, { focus: 1.4, aperture: 0.002, maxblur: 0.005 }));
|
|
918
|
+
}
|
|
919
|
+
composer.addPass(new OutputPass_js.OutputPass());
|
|
920
|
+
return {
|
|
921
|
+
composer,
|
|
922
|
+
dispose: () => { composer.renderTarget1.dispose(); composer.renderTarget2.dispose(); },
|
|
923
|
+
setSize: (w, h) => composer.setSize(w, h),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const FPS_SAMPLE_SIZE = 60;
|
|
927
|
+
const FPS_THRESHOLD = 30;
|
|
928
|
+
class FpsMonitor {
|
|
929
|
+
constructor() {
|
|
930
|
+
this.samples = [];
|
|
931
|
+
this.lastTime = 0;
|
|
932
|
+
this.disabled = false;
|
|
933
|
+
}
|
|
934
|
+
tick(time) {
|
|
935
|
+
if (this.lastTime > 0) {
|
|
936
|
+
const delta = time - this.lastTime;
|
|
937
|
+
if (delta > 0)
|
|
938
|
+
this.samples.push(1000 / delta);
|
|
939
|
+
if (this.samples.length > FPS_SAMPLE_SIZE)
|
|
940
|
+
this.samples.shift();
|
|
941
|
+
}
|
|
942
|
+
this.lastTime = time;
|
|
943
|
+
if (this.samples.length >= FPS_SAMPLE_SIZE) {
|
|
944
|
+
const avg = this.samples.reduce((a, b) => a + b, 0) / this.samples.length;
|
|
945
|
+
if (avg < FPS_THRESHOLD) {
|
|
946
|
+
this.disabled = true;
|
|
947
|
+
console.warn(`[VRM] Post-processing auto-disabled (avg FPS: ${avg.toFixed(0)})`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
reset() {
|
|
952
|
+
this.samples = [];
|
|
953
|
+
this.lastTime = 0;
|
|
954
|
+
this.disabled = false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
let ktx2Loader = null;
|
|
959
|
+
function getKTX2Loader(renderer) {
|
|
960
|
+
if (!ktx2Loader) {
|
|
961
|
+
ktx2Loader = new KTX2Loader_js.KTX2Loader();
|
|
962
|
+
ktx2Loader.setTranscoderPath('/basis/');
|
|
963
|
+
ktx2Loader.detectSupport(renderer);
|
|
964
|
+
}
|
|
965
|
+
return ktx2Loader;
|
|
966
|
+
}
|
|
967
|
+
function loadEnvironment(renderer, url) {
|
|
968
|
+
const cleanUrl = url.split('?')[0].split('#')[0].toLowerCase();
|
|
969
|
+
// For blob URLs, the filename is passed as a hash fragment (e.g. blob:...#file.ktx2)
|
|
970
|
+
const hash = (url.split('#')[1] || '').toLowerCase();
|
|
971
|
+
const hashExt = hash.includes('.') ? '.' + hash.split('.').pop() : '';
|
|
972
|
+
const pmrem = new THREE__namespace.PMREMGenerator(renderer);
|
|
973
|
+
pmrem.compileEquirectangularShader();
|
|
974
|
+
const finalize = (texture) => {
|
|
975
|
+
const envMap = pmrem.fromEquirectangular(texture).texture;
|
|
976
|
+
texture.dispose();
|
|
977
|
+
pmrem.dispose();
|
|
978
|
+
return { envMap, dispose: () => envMap.dispose() };
|
|
979
|
+
};
|
|
980
|
+
if (cleanUrl.endsWith('.ktx2') || hashExt === '.ktx2') {
|
|
981
|
+
return new Promise((resolve, reject) => {
|
|
982
|
+
getKTX2Loader(renderer).load(url, (compressedTex) => {
|
|
983
|
+
// Compressed textures can't be flipped by WebGL on upload, so the
|
|
984
|
+
// top-down KTX2 data is inverted vs what PMREM expects. Render to
|
|
985
|
+
// an intermediate target with flipped v to decompress + correct.
|
|
986
|
+
const w = compressedTex.image.width, h = compressedTex.image.height;
|
|
987
|
+
const rt = new THREE__namespace.WebGLRenderTarget(w, h, { type: THREE__namespace.HalfFloatType });
|
|
988
|
+
const cam = new THREE__namespace.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
989
|
+
const mat = new THREE__namespace.ShaderMaterial({
|
|
990
|
+
uniforms: { tMap: { value: compressedTex } },
|
|
991
|
+
vertexShader: 'varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position,1.); }',
|
|
992
|
+
fragmentShader: 'uniform sampler2D tMap; varying vec2 vUv; void main(){ gl_FragColor=texture2D(tMap,vec2(vUv.x,1.-vUv.y)); }',
|
|
993
|
+
});
|
|
994
|
+
const quad = new THREE__namespace.Mesh(new THREE__namespace.PlaneGeometry(2, 2), mat);
|
|
995
|
+
const sc = new THREE__namespace.Scene();
|
|
996
|
+
sc.add(quad);
|
|
997
|
+
const prevRT = renderer.getRenderTarget();
|
|
998
|
+
renderer.setRenderTarget(rt);
|
|
999
|
+
renderer.render(sc, cam);
|
|
1000
|
+
renderer.setRenderTarget(prevRT);
|
|
1001
|
+
mat.dispose();
|
|
1002
|
+
quad.geometry.dispose();
|
|
1003
|
+
compressedTex.dispose();
|
|
1004
|
+
rt.texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
|
|
1005
|
+
const envMap = pmrem.fromEquirectangular(rt.texture).texture;
|
|
1006
|
+
rt.dispose();
|
|
1007
|
+
pmrem.dispose();
|
|
1008
|
+
resolve({ envMap, dispose: () => envMap.dispose() });
|
|
1009
|
+
}, undefined, reject);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
1013
|
+
const onLoad = (texture) => resolve(finalize(texture));
|
|
1014
|
+
if (cleanUrl.endsWith('.exr') || hashExt === '.exr') {
|
|
1015
|
+
new EXRLoader_js.EXRLoader().load(url, onLoad, undefined, reject);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
new HDRLoader_js.HDRLoader().load(url, onLoad, undefined, reject);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const DEFAULT_MAX_MODEL_SIZE = 30 * 1024 * 1024; // 30MB
|
|
1024
|
+
function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = 'https://xwqtyabmavpacejuypyp.supabase.co/storage/v1/object/public/vrm-templates/5705015963733407866.vrm', backgroundColor = 0xffffff, blendshapeMap, expressionOverrides, onModelLoad, maxModelSize = DEFAULT_MAX_MODEL_SIZE, cameraPosition, cameraTarget, cameraFov, lightIntensity, idleAnimationUrl, animationSpeed = 1, animationWeight = 1, animationCrossfade = 0.5, postProcessing, environmentUrl, environmentIntensity, environmentBlur, environmentZoom, onEnvironmentLoad, features, orbitAngle, orbitElevation, avatarRotation, onOrbitChange, cursorFollow = true, preserveDrawingBuffer = false, }) {
|
|
882
1025
|
const containerRef = react.useRef(null);
|
|
883
1026
|
const rendererRef = react.useRef(null);
|
|
884
1027
|
const sceneRef = react.useRef(null);
|
|
885
1028
|
const cameraRef = react.useRef(null);
|
|
886
1029
|
const vrmRef = react.useRef(null);
|
|
1030
|
+
const lightsRef = react.useRef(null);
|
|
1031
|
+
const onModelLoadRef = react.useRef(onModelLoad);
|
|
1032
|
+
onModelLoadRef.current = onModelLoad;
|
|
1033
|
+
const animSpeedRef = react.useRef(animationSpeed);
|
|
1034
|
+
animSpeedRef.current = animationSpeed;
|
|
1035
|
+
const animWeightRef = react.useRef(animationWeight);
|
|
1036
|
+
animWeightRef.current = animationWeight;
|
|
1037
|
+
const animCrossfadeRef = react.useRef(animationCrossfade);
|
|
1038
|
+
animCrossfadeRef.current = animationCrossfade;
|
|
1039
|
+
const exprOverridesRef = react.useRef(expressionOverrides);
|
|
1040
|
+
exprOverridesRef.current = expressionOverrides;
|
|
1041
|
+
const featuresRef = react.useRef(features);
|
|
1042
|
+
featuresRef.current = features;
|
|
1043
|
+
const envZoomRef = react.useRef(environmentZoom);
|
|
1044
|
+
envZoomRef.current = environmentZoom;
|
|
1045
|
+
const onEnvLoadRef = react.useRef(onEnvironmentLoad);
|
|
1046
|
+
onEnvLoadRef.current = onEnvironmentLoad;
|
|
887
1047
|
const [loading, setLoading] = react.useState(true);
|
|
888
1048
|
const [error, setError] = react.useState(null);
|
|
889
1049
|
const animationFrameRef = react.useRef(0);
|
|
890
1050
|
const animationTimeRef = react.useRef(0);
|
|
1051
|
+
const mixerRef = react.useRef(null);
|
|
1052
|
+
const clipActionRef = react.useRef(null);
|
|
1053
|
+
const composerRef = react.useRef(null);
|
|
1054
|
+
const envMapRef = react.useRef(null);
|
|
1055
|
+
const bgSceneRef = react.useRef(new THREE__namespace.Scene());
|
|
1056
|
+
react.useRef(null);
|
|
1057
|
+
const fpsMonitorRef = react.useRef(new FpsMonitor());
|
|
1058
|
+
const orbitRef = react.useRef({ angle: 0, targetAngle: 0, elevation: 0, targetElevation: 0, zoomOffset: 0, dragging: false, lastX: 0, lastY: 0 });
|
|
1059
|
+
const orbitAnglePropRef = react.useRef(orbitAngle);
|
|
1060
|
+
orbitAnglePropRef.current = orbitAngle;
|
|
1061
|
+
const onOrbitChangeRef = react.useRef(onOrbitChange);
|
|
1062
|
+
onOrbitChangeRef.current = onOrbitChange;
|
|
1063
|
+
const baseCamRef = react.useRef({ distance: 1.2, height: 1.4, targetY: 1.3 });
|
|
1064
|
+
const idleRef = react.useRef({ blinkNext: 2 + Math.random() * 4, blinkPhase: 0, blinkStart: 0,
|
|
1065
|
+
happy: 0, happyTarget: 0, browInner: 0, browTarget: 0, cheek: 0, cheekTarget: 0, microNext: 3,
|
|
1066
|
+
eyeX: 0, eyeY: 0, eyeTargetX: 0, eyeTargetY: 0, eyeNext: 1 });
|
|
1067
|
+
const isA2FActiveRef = react.useRef(false);
|
|
1068
|
+
const idleMorphsRef = react.useRef([]);
|
|
1069
|
+
const springSettleFramesRef = react.useRef(0);
|
|
1070
|
+
const cursorFollowRef = react.useRef(cursorFollow);
|
|
1071
|
+
cursorFollowRef.current = cursorFollow;
|
|
1072
|
+
const cursorRef = react.useRef({ x: 0, y: 0, active: false });
|
|
891
1073
|
const applyBlendshapes = react.useCallback((vrm, shapes) => {
|
|
892
1074
|
const arkitMap = {};
|
|
893
1075
|
ARKIT_BLENDSHAPES.forEach((name, i) => {
|
|
894
1076
|
arkitMap[name] = shapes[i] || 0;
|
|
895
1077
|
});
|
|
896
|
-
const mouthOpen = arkitMap.jawOpen || 0;
|
|
1078
|
+
const mouthOpen = arkitMap.jawOpen || 0, funnel = arkitMap.mouthFunnel || 0;
|
|
897
1079
|
const smile = ((arkitMap.mouthSmileLeft || 0) + (arkitMap.mouthSmileRight || 0)) / 2;
|
|
898
|
-
const
|
|
899
|
-
const blinkLeft = arkitMap.eyeBlinkLeft || 0;
|
|
900
|
-
const blinkRight = arkitMap.eyeBlinkRight || 0;
|
|
1080
|
+
const blinkLeft = arkitMap.eyeBlinkLeft || 0, blinkRight = arkitMap.eyeBlinkRight || 0;
|
|
901
1081
|
if (vrm.expressionManager) {
|
|
902
1082
|
vrm.expressionManager.expressions.forEach(exp => {
|
|
903
1083
|
vrm.expressionManager?.setValue(exp.expressionName, 0);
|
|
904
1084
|
});
|
|
905
|
-
const
|
|
1085
|
+
const trySet = (name, value) => {
|
|
906
1086
|
try {
|
|
907
1087
|
vrm.expressionManager?.setValue(name, Math.min(Math.max(value, 0), 1));
|
|
908
1088
|
}
|
|
909
|
-
catch {
|
|
910
|
-
// Expression doesn't exist
|
|
911
|
-
}
|
|
1089
|
+
catch { }
|
|
912
1090
|
};
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1091
|
+
trySet('aa', mouthOpen * 1.5);
|
|
1092
|
+
trySet('happy', smile * 2);
|
|
1093
|
+
trySet('blink', (blinkLeft + blinkRight) / 2);
|
|
1094
|
+
trySet('blinkLeft', blinkLeft);
|
|
1095
|
+
trySet('blinkRight', blinkRight);
|
|
1096
|
+
trySet('ou', funnel);
|
|
1097
|
+
try {
|
|
1098
|
+
vrm.expressionManager.update();
|
|
1099
|
+
}
|
|
1100
|
+
catch { }
|
|
920
1101
|
}
|
|
921
1102
|
vrm.scene.traverse((obj) => {
|
|
922
1103
|
if (obj.isMesh) {
|
|
@@ -944,7 +1125,8 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
|
|
|
944
1125
|
renderer = new THREE__namespace.WebGLRenderer({
|
|
945
1126
|
antialias: true,
|
|
946
1127
|
alpha: true,
|
|
947
|
-
powerPreference: 'high-performance'
|
|
1128
|
+
powerPreference: 'high-performance',
|
|
1129
|
+
preserveDrawingBuffer,
|
|
948
1130
|
});
|
|
949
1131
|
}
|
|
950
1132
|
catch {
|
|
@@ -958,20 +1140,21 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
|
|
|
958
1140
|
container.appendChild(renderer.domElement);
|
|
959
1141
|
rendererRef.current = renderer;
|
|
960
1142
|
const scene = new THREE__namespace.Scene();
|
|
961
|
-
scene.background = new THREE__namespace.Color(backgroundColor);
|
|
1143
|
+
scene.background = backgroundColor === null ? null : new THREE__namespace.Color(backgroundColor ?? 0xffffff);
|
|
962
1144
|
sceneRef.current = scene;
|
|
963
1145
|
const camera = new THREE__namespace.PerspectiveCamera(30, width / height, 0.1, 20);
|
|
964
1146
|
camera.position.set(0, 1.4, 1.2);
|
|
965
1147
|
camera.lookAt(0, 1.3, 0);
|
|
966
1148
|
cameraRef.current = camera;
|
|
967
|
-
const ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0
|
|
1149
|
+
const ambientLight = new THREE__namespace.AmbientLight(0xffffff, 1.0);
|
|
968
1150
|
scene.add(ambientLight);
|
|
969
|
-
const directionalLight = new THREE__namespace.DirectionalLight(0xffffff,
|
|
1151
|
+
const directionalLight = new THREE__namespace.DirectionalLight(0xffffff, 1.2);
|
|
970
1152
|
directionalLight.position.set(1, 1, 1);
|
|
971
1153
|
scene.add(directionalLight);
|
|
972
|
-
const backLight = new THREE__namespace.DirectionalLight(0x4a9eff, 0.
|
|
1154
|
+
const backLight = new THREE__namespace.DirectionalLight(0x4a9eff, 0.4);
|
|
973
1155
|
backLight.position.set(-1, 1, -1);
|
|
974
1156
|
scene.add(backLight);
|
|
1157
|
+
lightsRef.current = { ambient: ambientLight, directional: directionalLight, back: backLight };
|
|
975
1158
|
let cancelled = false;
|
|
976
1159
|
// Size guard: fire HEAD check in parallel — if model is too large, cancel before render
|
|
977
1160
|
if (maxModelSize > 0) {
|
|
@@ -986,7 +1169,7 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
|
|
|
986
1169
|
const errMsg = `Model too large (${sizeMB}MB, limit ${limitMB}MB)`;
|
|
987
1170
|
setError(errMsg);
|
|
988
1171
|
setLoading(false);
|
|
989
|
-
|
|
1172
|
+
onModelLoadRef.current?.({
|
|
990
1173
|
supported: 0, missing: [...ARKIT_BLENDSHAPES], warnings: [errMsg],
|
|
991
1174
|
modelStats: { vertexCount: 0, textureCount: 0, morphTargetCount: 0 },
|
|
992
1175
|
});
|
|
@@ -1002,12 +1185,49 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
|
|
|
1002
1185
|
if (vrm) {
|
|
1003
1186
|
scene.add(vrm.scene);
|
|
1004
1187
|
vrmRef.current = vrm;
|
|
1188
|
+
const mc = [];
|
|
1189
|
+
vrm.scene.traverse((o) => {
|
|
1190
|
+
const mesh = o;
|
|
1191
|
+
if (!mesh.isMesh)
|
|
1192
|
+
return;
|
|
1193
|
+
const d = mesh.morphTargetDictionary, inf = mesh.morphTargetInfluences;
|
|
1194
|
+
if (!d || !inf)
|
|
1195
|
+
return;
|
|
1196
|
+
const e = { inf };
|
|
1197
|
+
if (d['browInnerUp'] !== undefined)
|
|
1198
|
+
e.brow = d['browInnerUp'];
|
|
1199
|
+
if (d['cheekSquintLeft'] !== undefined)
|
|
1200
|
+
e.cheekL = d['cheekSquintLeft'];
|
|
1201
|
+
if (d['cheekSquintRight'] !== undefined)
|
|
1202
|
+
e.cheekR = d['cheekSquintRight'];
|
|
1203
|
+
if (e.brow !== undefined || e.cheekL !== undefined)
|
|
1204
|
+
mc.push(e);
|
|
1205
|
+
});
|
|
1206
|
+
idleMorphsRef.current = mc;
|
|
1207
|
+
// Apply idle pose immediately so the model never renders in T-pose
|
|
1208
|
+
const humanoid = vrm.humanoid;
|
|
1209
|
+
if (humanoid) {
|
|
1210
|
+
const b = (n) => humanoid.getNormalizedBoneNode(n);
|
|
1211
|
+
const lArm = b(threeVrm.VRMHumanBoneName.LeftUpperArm), rArm = b(threeVrm.VRMHumanBoneName.RightUpperArm);
|
|
1212
|
+
if (lArm)
|
|
1213
|
+
lArm.rotation.z = -1;
|
|
1214
|
+
if (rArm)
|
|
1215
|
+
rArm.rotation.z = 1.0;
|
|
1216
|
+
}
|
|
1217
|
+
if (vrm.lookAt) {
|
|
1218
|
+
const proxy = new threeVrmAnimation.VRMLookAtQuaternionProxy(vrm.lookAt);
|
|
1219
|
+
proxy.name = 'VRMLookAtQuaternionProxy';
|
|
1220
|
+
vrm.scene.add(proxy);
|
|
1221
|
+
}
|
|
1222
|
+
vrm.update(0);
|
|
1223
|
+
vrm.springBoneManager?.reset();
|
|
1224
|
+
springSettleFramesRef.current = 30;
|
|
1005
1225
|
setLoading(false);
|
|
1006
1226
|
const report = buildCompatibilityReport(vrm);
|
|
1007
1227
|
if (report.warnings.length > 0) {
|
|
1008
1228
|
console.warn('[VRM] Compatibility warnings:', report.warnings);
|
|
1009
1229
|
}
|
|
1010
|
-
|
|
1230
|
+
onModelLoadRef.current?.(report);
|
|
1011
1231
|
}
|
|
1012
1232
|
}, () => { }, (err) => {
|
|
1013
1233
|
if (cancelled)
|
|
@@ -1021,79 +1241,501 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
|
|
|
1021
1241
|
setError(detail);
|
|
1022
1242
|
setLoading(false);
|
|
1023
1243
|
});
|
|
1024
|
-
const
|
|
1244
|
+
const timer = new THREE__namespace.Timer();
|
|
1025
1245
|
const animate = () => {
|
|
1026
1246
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
1027
|
-
|
|
1247
|
+
timer.update();
|
|
1248
|
+
const delta = timer.getDelta();
|
|
1028
1249
|
animationTimeRef.current += delta;
|
|
1029
1250
|
if (vrmRef.current) {
|
|
1030
|
-
|
|
1251
|
+
try {
|
|
1252
|
+
vrmRef.current.update(delta);
|
|
1253
|
+
}
|
|
1254
|
+
catch { }
|
|
1255
|
+
if (featuresRef.current?.springBones === false || springSettleFramesRef.current > 0) {
|
|
1256
|
+
vrmRef.current.springBoneManager?.reset();
|
|
1257
|
+
if (springSettleFramesRef.current > 0)
|
|
1258
|
+
springSettleFramesRef.current--;
|
|
1259
|
+
}
|
|
1260
|
+
if (mixerRef.current)
|
|
1261
|
+
mixerRef.current.update(delta);
|
|
1031
1262
|
const humanoid = vrmRef.current.humanoid;
|
|
1032
|
-
if (humanoid) {
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1263
|
+
if (featuresRef.current?.idleAnimation !== false && humanoid && !clipActionRef.current) {
|
|
1264
|
+
const t = animationTimeRef.current, br = t * 1.2, sw = t * 0.5, hb = t * 0.7;
|
|
1265
|
+
const b = (n) => humanoid.getNormalizedBoneNode(n);
|
|
1266
|
+
const spine = b(threeVrm.VRMHumanBoneName.Spine);
|
|
1267
|
+
if (spine) {
|
|
1268
|
+
spine.rotation.x = Math.sin(br) * 0.03;
|
|
1269
|
+
spine.rotation.z = Math.sin(sw) * 0.006;
|
|
1270
|
+
}
|
|
1271
|
+
const chest = b(threeVrm.VRMHumanBoneName.Chest);
|
|
1272
|
+
if (chest) {
|
|
1273
|
+
chest.rotation.x = Math.sin(br + 0.3) * 0.015;
|
|
1274
|
+
chest.rotation.z = Math.sin(sw + 0.5) * 0.004;
|
|
1044
1275
|
}
|
|
1045
|
-
const
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
1048
|
-
|
|
1276
|
+
const neck = b(threeVrm.VRMHumanBoneName.Neck);
|
|
1277
|
+
if (neck) {
|
|
1278
|
+
neck.rotation.x = Math.sin(hb) * 0.015;
|
|
1279
|
+
neck.rotation.y = Math.sin(sw * 0.7) * 0.01;
|
|
1049
1280
|
}
|
|
1050
|
-
const
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1281
|
+
const head = b(threeVrm.VRMHumanBoneName.Head);
|
|
1282
|
+
if (head) {
|
|
1283
|
+
head.rotation.x = Math.sin(hb + 0.5) * 0.006;
|
|
1284
|
+
head.rotation.y = Math.sin(sw * 0.6 + 0.8) * 0.006;
|
|
1054
1285
|
}
|
|
1055
|
-
const
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1286
|
+
const lSh = b(threeVrm.VRMHumanBoneName.LeftShoulder), rSh = b(threeVrm.VRMHumanBoneName.RightShoulder);
|
|
1287
|
+
if (lSh)
|
|
1288
|
+
lSh.rotation.z = Math.sin(br) * 0.006;
|
|
1289
|
+
if (rSh)
|
|
1290
|
+
rSh.rotation.z = -Math.sin(br) * 0.006;
|
|
1291
|
+
const lArm = b(threeVrm.VRMHumanBoneName.LeftUpperArm), rArm = b(threeVrm.VRMHumanBoneName.RightUpperArm);
|
|
1292
|
+
if (lArm)
|
|
1293
|
+
lArm.rotation.z = -1 + Math.sin(sw * 0.4) * 0.02;
|
|
1294
|
+
if (rArm)
|
|
1295
|
+
rArm.rotation.z = 1.0 + Math.sin(sw * 0.4 + 0.5) * 0.02;
|
|
1296
|
+
}
|
|
1297
|
+
if (featuresRef.current?.microExpressions !== false && vrmRef.current.expressionManager && !isA2FActiveRef.current &&
|
|
1298
|
+
!Object.keys(exprOverridesRef.current ?? {}).length) {
|
|
1299
|
+
const idle = idleRef.current, t = animationTimeRef.current;
|
|
1300
|
+
let bv = 0;
|
|
1301
|
+
if (idle.blinkPhase === 0 && t >= idle.blinkNext) {
|
|
1302
|
+
idle.blinkPhase = 1;
|
|
1303
|
+
idle.blinkStart = t;
|
|
1059
1304
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1305
|
+
if (idle.blinkPhase === 1) {
|
|
1306
|
+
bv = Math.min((t - idle.blinkStart) / 0.15, 1);
|
|
1307
|
+
if (bv >= 1) {
|
|
1308
|
+
idle.blinkPhase = 2;
|
|
1309
|
+
idle.blinkStart = t;
|
|
1310
|
+
}
|
|
1064
1311
|
}
|
|
1065
|
-
if (
|
|
1066
|
-
|
|
1312
|
+
else if (idle.blinkPhase === 2) {
|
|
1313
|
+
bv = 1 - Math.min((t - idle.blinkStart) / 0.15, 1);
|
|
1314
|
+
if (bv <= 0) {
|
|
1315
|
+
idle.blinkPhase = 0;
|
|
1316
|
+
idle.blinkNext = t + 2 + Math.random() * 4;
|
|
1317
|
+
}
|
|
1067
1318
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1319
|
+
if (t >= idle.microNext) {
|
|
1320
|
+
idle.happyTarget = Math.random() * 0.1;
|
|
1321
|
+
idle.browTarget = Math.random() * 0.08;
|
|
1322
|
+
idle.cheekTarget = Math.random() * 0.05;
|
|
1323
|
+
idle.microNext = t + 3 + Math.random() * 5;
|
|
1072
1324
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1325
|
+
idle.happy += (idle.happyTarget - idle.happy) * 0.02;
|
|
1326
|
+
idle.browInner += (idle.browTarget - idle.browInner) * 0.02;
|
|
1327
|
+
idle.cheek += (idle.cheekTarget - idle.cheek) * 0.02;
|
|
1328
|
+
const cursor = cursorRef.current;
|
|
1329
|
+
if (cursorFollowRef.current && cursor.active) {
|
|
1330
|
+
idle.eyeTargetX = cursor.x * 0.26; // ±15° horizontal (0.26 rad ≈ 15°)
|
|
1331
|
+
idle.eyeTargetY = cursor.y * 0.17; // ±10° vertical (0.17 rad ≈ 10°)
|
|
1332
|
+
}
|
|
1333
|
+
else if (t >= idle.eyeNext) {
|
|
1334
|
+
idle.eyeTargetX = (Math.random() - 0.5) * 0.06;
|
|
1335
|
+
idle.eyeTargetY = (Math.random() - 0.5) * 0.04;
|
|
1336
|
+
idle.eyeNext = t + 1.5 + Math.random() * 3;
|
|
1337
|
+
}
|
|
1338
|
+
idle.eyeX += (idle.eyeTargetX - idle.eyeX) * 0.06;
|
|
1339
|
+
idle.eyeY += (idle.eyeTargetY - idle.eyeY) * 0.06;
|
|
1340
|
+
const em = vrmRef.current.expressionManager;
|
|
1341
|
+
try {
|
|
1342
|
+
em.setValue('blink', Math.max(bv, 0));
|
|
1343
|
+
}
|
|
1344
|
+
catch { }
|
|
1345
|
+
try {
|
|
1346
|
+
em.setValue('happy', Math.max(idle.happy, 0));
|
|
1347
|
+
}
|
|
1348
|
+
catch { }
|
|
1349
|
+
try {
|
|
1350
|
+
em.update();
|
|
1351
|
+
}
|
|
1352
|
+
catch { }
|
|
1353
|
+
for (const m of idleMorphsRef.current) {
|
|
1354
|
+
if (m.brow !== undefined)
|
|
1355
|
+
m.inf[m.brow] = idle.browInner;
|
|
1356
|
+
if (m.cheekL !== undefined)
|
|
1357
|
+
m.inf[m.cheekL] = idle.cheek;
|
|
1358
|
+
if (m.cheekR !== undefined)
|
|
1359
|
+
m.inf[m.cheekR] = idle.cheek;
|
|
1360
|
+
}
|
|
1361
|
+
if (humanoid) {
|
|
1362
|
+
for (const bn of [threeVrm.VRMHumanBoneName.LeftEye, threeVrm.VRMHumanBoneName.RightEye]) {
|
|
1363
|
+
const eye = humanoid.getNormalizedBoneNode(bn);
|
|
1364
|
+
if (eye) {
|
|
1365
|
+
eye.rotation.x = idle.eyeY;
|
|
1366
|
+
eye.rotation.y = idle.eyeX;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1075
1369
|
}
|
|
1076
1370
|
}
|
|
1077
1371
|
}
|
|
1078
|
-
|
|
1372
|
+
// Smooth lerp for spherical orbit (azimuth + elevation)
|
|
1373
|
+
const orbit = orbitRef.current;
|
|
1374
|
+
const elevDiff = orbit.targetElevation - orbit.elevation;
|
|
1375
|
+
const angleDiff = orbit.targetAngle - orbit.angle;
|
|
1376
|
+
if (Math.abs(elevDiff) > 0.0005 || Math.abs(angleDiff) > 0.0005) {
|
|
1377
|
+
orbit.elevation += elevDiff * 0.1;
|
|
1378
|
+
orbit.angle += angleDiff * 0.1;
|
|
1379
|
+
const base = baseCamRef.current;
|
|
1380
|
+
const d = Math.max(0.3, base.distance + orbit.zoomOffset);
|
|
1381
|
+
const cosEl = Math.cos(orbit.elevation);
|
|
1382
|
+
const sinEl = Math.sin(orbit.elevation);
|
|
1383
|
+
camera.position.set(Math.sin(orbit.angle) * cosEl * d, base.height + sinEl * d, Math.cos(orbit.angle) * cosEl * d);
|
|
1384
|
+
camera.lookAt(0, base.targetY, 0);
|
|
1385
|
+
}
|
|
1386
|
+
const pipeline = composerRef.current;
|
|
1387
|
+
const ez = envZoomRef.current;
|
|
1388
|
+
const hasBgZoom = ez && ez !== 1 && scene.background && envMapRef.current;
|
|
1389
|
+
if (hasBgZoom && !pipeline) {
|
|
1390
|
+
const baseFov = camera.fov;
|
|
1391
|
+
const bgScene = bgSceneRef.current;
|
|
1392
|
+
bgScene.backgroundBlurriness = scene.backgroundBlurriness;
|
|
1393
|
+
bgScene.backgroundIntensity = scene.backgroundIntensity ?? 1;
|
|
1394
|
+
camera.fov = baseFov / ez;
|
|
1395
|
+
camera.updateProjectionMatrix();
|
|
1396
|
+
renderer.render(bgScene, camera);
|
|
1397
|
+
camera.fov = baseFov;
|
|
1398
|
+
camera.updateProjectionMatrix();
|
|
1399
|
+
const savedBg = scene.background;
|
|
1400
|
+
scene.background = null;
|
|
1401
|
+
renderer.autoClear = false;
|
|
1402
|
+
renderer.clearDepth();
|
|
1403
|
+
renderer.render(scene, camera);
|
|
1404
|
+
renderer.autoClear = true;
|
|
1405
|
+
scene.background = savedBg;
|
|
1406
|
+
}
|
|
1407
|
+
else if (pipeline && !fpsMonitorRef.current.disabled) {
|
|
1408
|
+
pipeline.composer.render();
|
|
1409
|
+
fpsMonitorRef.current.tick(performance.now());
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
renderer.render(scene, camera);
|
|
1413
|
+
}
|
|
1079
1414
|
};
|
|
1080
1415
|
animate();
|
|
1081
1416
|
return () => {
|
|
1082
1417
|
cancelled = true;
|
|
1083
1418
|
cancelAnimationFrame(animationFrameRef.current);
|
|
1419
|
+
clipActionRef.current = null;
|
|
1420
|
+
mixerRef.current = null;
|
|
1421
|
+
composerRef.current?.dispose();
|
|
1422
|
+
composerRef.current = null;
|
|
1423
|
+
envMapRef.current?.dispose();
|
|
1424
|
+
envMapRef.current = null;
|
|
1084
1425
|
renderer.dispose();
|
|
1085
1426
|
if (container && renderer.domElement) {
|
|
1086
1427
|
container.removeChild(renderer.domElement);
|
|
1087
1428
|
}
|
|
1088
1429
|
};
|
|
1089
|
-
}, [width, height, modelUrl,
|
|
1430
|
+
}, [width, height, modelUrl, maxModelSize, preserveDrawingBuffer]);
|
|
1431
|
+
react.useEffect(() => {
|
|
1432
|
+
composerRef.current?.dispose();
|
|
1433
|
+
composerRef.current = null;
|
|
1434
|
+
fpsMonitorRef.current.reset();
|
|
1435
|
+
const renderer = rendererRef.current, scene = sceneRef.current, camera = cameraRef.current;
|
|
1436
|
+
const pp = features?.postProcessing ?? postProcessing;
|
|
1437
|
+
if (!renderer || !scene || !camera || !pp)
|
|
1438
|
+
return;
|
|
1439
|
+
const hasAnyEffect = (pp.bloom && pp.bloom > 0) || (pp.ao && pp.ao > 0) || pp.dof;
|
|
1440
|
+
if (!hasAnyEffect)
|
|
1441
|
+
return;
|
|
1442
|
+
composerRef.current = createPostProcessing(renderer, scene, camera, pp, width, height);
|
|
1443
|
+
return () => { composerRef.current?.dispose(); composerRef.current = null; };
|
|
1444
|
+
}, [postProcessing, features, width, height]);
|
|
1445
|
+
// Load/unload .vrma animation clip — crossfade between clips
|
|
1446
|
+
react.useEffect(() => {
|
|
1447
|
+
const vrm = vrmRef.current;
|
|
1448
|
+
const fade = animCrossfadeRef.current;
|
|
1449
|
+
if (!idleAnimationUrl) {
|
|
1450
|
+
const old = clipActionRef.current;
|
|
1451
|
+
if (old) {
|
|
1452
|
+
old.fadeOut(fade);
|
|
1453
|
+
setTimeout(() => {
|
|
1454
|
+
old.stop();
|
|
1455
|
+
if (clipActionRef.current === old)
|
|
1456
|
+
clipActionRef.current = null;
|
|
1457
|
+
if (mixerRef.current && !clipActionRef.current) {
|
|
1458
|
+
mixerRef.current.stopAllAction();
|
|
1459
|
+
mixerRef.current = null;
|
|
1460
|
+
}
|
|
1461
|
+
}, fade * 1000);
|
|
1462
|
+
}
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (!vrm)
|
|
1466
|
+
return;
|
|
1467
|
+
let cancelled = false;
|
|
1468
|
+
const loader = new GLTFLoader_js.GLTFLoader();
|
|
1469
|
+
loader.register((parser) => new threeVrmAnimation.VRMAnimationLoaderPlugin(parser));
|
|
1470
|
+
const origWarn = console.warn;
|
|
1471
|
+
console.warn = (...args) => {
|
|
1472
|
+
if (typeof args[0] === 'string' && args[0].includes('specVersion'))
|
|
1473
|
+
return;
|
|
1474
|
+
origWarn.apply(console, args);
|
|
1475
|
+
};
|
|
1476
|
+
loader.load(idleAnimationUrl, (gltf) => {
|
|
1477
|
+
console.warn = origWarn;
|
|
1478
|
+
if (cancelled || !vrmRef.current)
|
|
1479
|
+
return;
|
|
1480
|
+
const animations = gltf.userData.vrmAnimations;
|
|
1481
|
+
if (!animations || animations.length === 0)
|
|
1482
|
+
return;
|
|
1483
|
+
const mixer = mixerRef.current ?? new THREE__namespace.AnimationMixer(vrmRef.current.scene);
|
|
1484
|
+
mixerRef.current = mixer;
|
|
1485
|
+
const clip = threeVrmAnimation.createVRMAnimationClip(animations[0], vrmRef.current);
|
|
1486
|
+
const newAction = mixer.clipAction(clip);
|
|
1487
|
+
newAction.setLoop(THREE__namespace.LoopPingPong, Infinity);
|
|
1488
|
+
newAction.timeScale = animSpeedRef.current;
|
|
1489
|
+
newAction.setEffectiveWeight(animWeightRef.current);
|
|
1490
|
+
const oldAction = clipActionRef.current;
|
|
1491
|
+
const cf = animCrossfadeRef.current;
|
|
1492
|
+
if (oldAction) {
|
|
1493
|
+
newAction.reset().play();
|
|
1494
|
+
oldAction.crossFadeTo(newAction, cf, true);
|
|
1495
|
+
setTimeout(() => { oldAction.stop(); mixer.uncacheAction(oldAction.getClip()); }, cf * 1000);
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
newAction.reset().fadeIn(cf).play();
|
|
1499
|
+
}
|
|
1500
|
+
clipActionRef.current = newAction;
|
|
1501
|
+
}, () => { }, (err) => {
|
|
1502
|
+
console.warn = origWarn;
|
|
1503
|
+
if (cancelled)
|
|
1504
|
+
return;
|
|
1505
|
+
console.warn('[VRM] Failed to load animation clip:', err);
|
|
1506
|
+
});
|
|
1507
|
+
return () => { cancelled = true; };
|
|
1508
|
+
}, [idleAnimationUrl, loading]);
|
|
1509
|
+
// Update animation speed/weight in real time
|
|
1510
|
+
react.useEffect(() => {
|
|
1511
|
+
if (clipActionRef.current)
|
|
1512
|
+
clipActionRef.current.timeScale = animationSpeed;
|
|
1513
|
+
}, [animationSpeed]);
|
|
1514
|
+
react.useEffect(() => {
|
|
1515
|
+
if (clipActionRef.current)
|
|
1516
|
+
clipActionRef.current.setEffectiveWeight(animationWeight);
|
|
1517
|
+
}, [animationWeight]);
|
|
1518
|
+
// Update background in-place (skip when HDRI active)
|
|
1519
|
+
react.useEffect(() => {
|
|
1520
|
+
if (sceneRef.current && !environmentUrl) {
|
|
1521
|
+
sceneRef.current.background = backgroundColor === null ? null : new THREE__namespace.Color(backgroundColor ?? 0xffffff);
|
|
1522
|
+
}
|
|
1523
|
+
}, [backgroundColor, environmentUrl]);
|
|
1524
|
+
// HDRI environment map loading — keep old envMap visible until new one is ready
|
|
1525
|
+
react.useEffect(() => {
|
|
1526
|
+
const scene = sceneRef.current, renderer = rendererRef.current;
|
|
1527
|
+
if (!scene || !renderer || !environmentUrl) {
|
|
1528
|
+
envMapRef.current?.dispose();
|
|
1529
|
+
envMapRef.current = null;
|
|
1530
|
+
if (scene) {
|
|
1531
|
+
scene.environment = null;
|
|
1532
|
+
scene.background = null;
|
|
1533
|
+
scene.backgroundBlurriness = 0;
|
|
1534
|
+
scene.environmentIntensity = 1;
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
let cancelled = false;
|
|
1539
|
+
const prev = envMapRef.current;
|
|
1540
|
+
loadEnvironment(renderer, environmentUrl).then(r => {
|
|
1541
|
+
if (cancelled) {
|
|
1542
|
+
r.dispose();
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
prev?.dispose();
|
|
1546
|
+
scene.environment = r.envMap;
|
|
1547
|
+
scene.background = r.envMap;
|
|
1548
|
+
scene.backgroundBlurriness = environmentBlur ?? 0;
|
|
1549
|
+
scene.environmentIntensity = environmentIntensity ?? 1;
|
|
1550
|
+
scene.backgroundIntensity = environmentIntensity ?? 1;
|
|
1551
|
+
bgSceneRef.current.background = r.envMap;
|
|
1552
|
+
envMapRef.current = { dispose: r.dispose, envMap: r.envMap };
|
|
1553
|
+
onEnvLoadRef.current?.();
|
|
1554
|
+
}).catch(e => console.warn('[VRM] HDRI load failed:', e));
|
|
1555
|
+
return () => { cancelled = true; };
|
|
1556
|
+
}, [environmentUrl]);
|
|
1557
|
+
// Update HDRI intensity/blur without reloading
|
|
1558
|
+
react.useEffect(() => {
|
|
1559
|
+
if (!sceneRef.current || !envMapRef.current)
|
|
1560
|
+
return;
|
|
1561
|
+
sceneRef.current.backgroundBlurriness = environmentBlur ?? 0;
|
|
1562
|
+
sceneRef.current.environmentIntensity = environmentIntensity ?? 1;
|
|
1563
|
+
sceneRef.current.backgroundIntensity = environmentIntensity ?? 1;
|
|
1564
|
+
}, [environmentIntensity, environmentBlur]);
|
|
1565
|
+
// Update camera base values and apply orbit
|
|
1566
|
+
react.useEffect(() => {
|
|
1567
|
+
const pos = cameraPosition ?? [0, 1.4, 1.2];
|
|
1568
|
+
const target = cameraTarget ?? [0, 1.3, 0];
|
|
1569
|
+
baseCamRef.current = { distance: pos[2], height: pos[1], targetY: target[1] };
|
|
1570
|
+
if (orbitAnglePropRef.current === undefined) {
|
|
1571
|
+
orbitRef.current.angle = 0;
|
|
1572
|
+
orbitRef.current.targetAngle = 0;
|
|
1573
|
+
}
|
|
1574
|
+
orbitRef.current.zoomOffset = 0;
|
|
1575
|
+
if (orbitElevation === undefined) {
|
|
1576
|
+
orbitRef.current.elevation = 0;
|
|
1577
|
+
orbitRef.current.targetElevation = 0;
|
|
1578
|
+
}
|
|
1579
|
+
const camera = cameraRef.current;
|
|
1580
|
+
if (!camera)
|
|
1581
|
+
return;
|
|
1582
|
+
const d = baseCamRef.current.distance;
|
|
1583
|
+
camera.position.set(0, baseCamRef.current.height, d);
|
|
1584
|
+
camera.lookAt(0, baseCamRef.current.targetY, 0);
|
|
1585
|
+
if (cameraFov !== undefined && camera.fov !== cameraFov) {
|
|
1586
|
+
camera.fov = cameraFov;
|
|
1587
|
+
camera.updateProjectionMatrix();
|
|
1588
|
+
}
|
|
1589
|
+
}, [cameraPosition, cameraTarget, cameraFov]);
|
|
1590
|
+
// Prop-driven orbit angle
|
|
1591
|
+
react.useEffect(() => {
|
|
1592
|
+
if (orbitAngle !== undefined) {
|
|
1593
|
+
orbitRef.current.targetAngle = orbitAngle;
|
|
1594
|
+
}
|
|
1595
|
+
}, [orbitAngle]);
|
|
1596
|
+
// Prop-driven orbit elevation
|
|
1597
|
+
react.useEffect(() => {
|
|
1598
|
+
if (orbitElevation !== undefined) {
|
|
1599
|
+
orbitRef.current.targetElevation = orbitElevation;
|
|
1600
|
+
}
|
|
1601
|
+
}, [orbitElevation]);
|
|
1602
|
+
// Prop-driven avatar rotation
|
|
1603
|
+
react.useEffect(() => {
|
|
1604
|
+
if (avatarRotation !== undefined && vrmRef.current) {
|
|
1605
|
+
vrmRef.current.scene.rotation.y = avatarRotation;
|
|
1606
|
+
}
|
|
1607
|
+
}, [avatarRotation, loading]);
|
|
1608
|
+
// Mouse orbit (horizontal only) + scroll zoom
|
|
1609
|
+
react.useEffect(() => {
|
|
1610
|
+
const container = containerRef.current;
|
|
1611
|
+
if (!container)
|
|
1612
|
+
return;
|
|
1613
|
+
const applyCameraOrbit = () => {
|
|
1614
|
+
const camera = cameraRef.current;
|
|
1615
|
+
if (!camera)
|
|
1616
|
+
return;
|
|
1617
|
+
const base = baseCamRef.current;
|
|
1618
|
+
const orbit = orbitRef.current;
|
|
1619
|
+
const d = Math.max(0.3, base.distance + orbit.zoomOffset);
|
|
1620
|
+
const cosEl = Math.cos(orbit.elevation);
|
|
1621
|
+
const sinEl = Math.sin(orbit.elevation);
|
|
1622
|
+
camera.position.set(Math.sin(orbit.angle) * cosEl * d, base.height + sinEl * d, Math.cos(orbit.angle) * cosEl * d);
|
|
1623
|
+
camera.lookAt(0, base.targetY, 0);
|
|
1624
|
+
};
|
|
1625
|
+
const onWheel = (e) => {
|
|
1626
|
+
e.preventDefault();
|
|
1627
|
+
orbitRef.current.zoomOffset += e.deltaY * 0.002;
|
|
1628
|
+
applyCameraOrbit();
|
|
1629
|
+
};
|
|
1630
|
+
const onMouseDown = (e) => {
|
|
1631
|
+
orbitRef.current.dragging = true;
|
|
1632
|
+
orbitRef.current.lastX = e.clientX;
|
|
1633
|
+
orbitRef.current.lastY = e.clientY;
|
|
1634
|
+
};
|
|
1635
|
+
const onMouseMove = (e) => {
|
|
1636
|
+
if (!orbitRef.current.dragging)
|
|
1637
|
+
return;
|
|
1638
|
+
const dx = e.clientX - orbitRef.current.lastX;
|
|
1639
|
+
const dy = e.clientY - orbitRef.current.lastY;
|
|
1640
|
+
orbitRef.current.lastX = e.clientX;
|
|
1641
|
+
orbitRef.current.lastY = e.clientY;
|
|
1642
|
+
orbitRef.current.angle -= dx * 0.005;
|
|
1643
|
+
orbitRef.current.targetAngle = orbitRef.current.angle;
|
|
1644
|
+
orbitRef.current.elevation = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, orbitRef.current.elevation + dy * 0.005));
|
|
1645
|
+
orbitRef.current.targetElevation = orbitRef.current.elevation;
|
|
1646
|
+
applyCameraOrbit();
|
|
1647
|
+
onOrbitChangeRef.current?.(orbitRef.current.angle, orbitRef.current.elevation);
|
|
1648
|
+
};
|
|
1649
|
+
const onMouseUp = () => {
|
|
1650
|
+
orbitRef.current.dragging = false;
|
|
1651
|
+
};
|
|
1652
|
+
const onKeyDown = (e) => {
|
|
1653
|
+
const o = orbitRef.current;
|
|
1654
|
+
if (e.key === 'ArrowUp') {
|
|
1655
|
+
e.preventDefault();
|
|
1656
|
+
o.targetElevation = Math.min(o.targetElevation + 0.1, Math.PI / 3);
|
|
1657
|
+
}
|
|
1658
|
+
else if (e.key === 'ArrowDown') {
|
|
1659
|
+
e.preventDefault();
|
|
1660
|
+
o.targetElevation = Math.max(o.targetElevation - 0.1, -Math.PI / 3);
|
|
1661
|
+
}
|
|
1662
|
+
else if (e.key === 'ArrowLeft') {
|
|
1663
|
+
e.preventDefault();
|
|
1664
|
+
o.targetAngle += 0.15;
|
|
1665
|
+
}
|
|
1666
|
+
else if (e.key === 'ArrowRight') {
|
|
1667
|
+
e.preventDefault();
|
|
1668
|
+
o.targetAngle -= 0.15;
|
|
1669
|
+
}
|
|
1670
|
+
onOrbitChangeRef.current?.(o.targetAngle, o.targetElevation);
|
|
1671
|
+
};
|
|
1672
|
+
const onCursorMove = (e) => {
|
|
1673
|
+
const rect = container.getBoundingClientRect();
|
|
1674
|
+
cursorRef.current.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
1675
|
+
cursorRef.current.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
|
|
1676
|
+
cursorRef.current.active = true;
|
|
1677
|
+
};
|
|
1678
|
+
const onCursorLeave = () => { cursorRef.current.active = false; };
|
|
1679
|
+
container.addEventListener('wheel', onWheel, { passive: false });
|
|
1680
|
+
container.addEventListener('mousedown', onMouseDown);
|
|
1681
|
+
container.addEventListener('mousemove', onCursorMove);
|
|
1682
|
+
container.addEventListener('mouseleave', onCursorLeave);
|
|
1683
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
1684
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
1685
|
+
window.addEventListener('keydown', onKeyDown);
|
|
1686
|
+
return () => {
|
|
1687
|
+
container.removeEventListener('wheel', onWheel);
|
|
1688
|
+
container.removeEventListener('mousedown', onMouseDown);
|
|
1689
|
+
container.removeEventListener('mousemove', onCursorMove);
|
|
1690
|
+
container.removeEventListener('mouseleave', onCursorLeave);
|
|
1691
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
1692
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
1693
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
1694
|
+
};
|
|
1695
|
+
}, []);
|
|
1696
|
+
// Update lighting in-place
|
|
1697
|
+
react.useEffect(() => {
|
|
1698
|
+
if (!lightsRef.current)
|
|
1699
|
+
return;
|
|
1700
|
+
const intensity = lightIntensity ?? 1;
|
|
1701
|
+
lightsRef.current.ambient.intensity = 1.0 * intensity;
|
|
1702
|
+
lightsRef.current.directional.intensity = 1.2 * intensity;
|
|
1703
|
+
lightsRef.current.back.intensity = 0.4 * intensity;
|
|
1704
|
+
}, [lightIntensity]);
|
|
1090
1705
|
react.useEffect(() => {
|
|
1706
|
+
isA2FActiveRef.current = blendshapes.some(v => v !== 0);
|
|
1091
1707
|
if (vrmRef.current && blendshapes.length > 0) {
|
|
1092
1708
|
applyBlendshapes(vrmRef.current, blendshapes);
|
|
1093
1709
|
}
|
|
1094
1710
|
}, [blendshapes, applyBlendshapes]);
|
|
1711
|
+
react.useEffect(() => {
|
|
1712
|
+
const vrm = vrmRef.current;
|
|
1713
|
+
if (!vrm?.expressionManager)
|
|
1714
|
+
return;
|
|
1715
|
+
const hasLiveData = blendshapes.some(v => v !== 0);
|
|
1716
|
+
if (hasLiveData)
|
|
1717
|
+
return;
|
|
1718
|
+
if (features?.expressionPresets === false)
|
|
1719
|
+
return;
|
|
1720
|
+
vrm.expressionManager.expressions.forEach(exp => {
|
|
1721
|
+
vrm.expressionManager.setValue(exp.expressionName, 0);
|
|
1722
|
+
});
|
|
1723
|
+
if (expressionOverrides) {
|
|
1724
|
+
for (const [name, value] of Object.entries(expressionOverrides)) {
|
|
1725
|
+
try {
|
|
1726
|
+
vrm.expressionManager.setValue(name, Math.min(Math.max(value, 0), 1));
|
|
1727
|
+
}
|
|
1728
|
+
catch { /* expression doesn't exist on this model */ }
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
vrm.expressionManager.update();
|
|
1733
|
+
}
|
|
1734
|
+
catch { }
|
|
1735
|
+
}, [expressionOverrides, blendshapes]);
|
|
1095
1736
|
return (jsxRuntime.jsxs("div", { className: "relative", style: { width, height }, children: [jsxRuntime.jsx("div", { ref: containerRef, className: "rounded-lg overflow-hidden" }), loading && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-800 rounded-lg", children: jsxRuntime.jsx("div", { className: "text-white text-sm", children: "Loading VRM avatar..." }) })), error && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg", children: jsxRuntime.jsx("div", { className: "text-white text-sm", children: error }) }))] }));
|
|
1096
1737
|
}
|
|
1097
1738
|
|
|
1098
1739
|
exports.VRMAvatar = VRMAvatar;
|
|
1740
|
+
exports.VROID_BLENDSHAPE_MAP = VROID_BLENDSHAPE_MAP;
|
|
1099
1741
|
exports.useAStackCSR = useAStackCSR;
|