Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c225ca3a5 |
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
evidence/_video/
|
||||
public/bundle.js
|
||||
@@ -0,0 +1,46 @@
|
||||
# HEL-702 · 独立3D角色换装/动作/环境 技术验证包
|
||||
|
||||
隔离试验目录:不依赖、不修改生产 `web/`,未部署。所有结论基于本目录实际运行结果(2026-10-20)。
|
||||
|
||||
## 结论速览(均已实测,非设想)
|
||||
|
||||
| 验证项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| VRM 角色浏览器加载 | ✅ 两个模型(pixiv/VirtualCast)加载成功,152ms | `evidence/runtime-stats.json`, 截图01/09 |
|
||||
| 换装(非改色) | ✅ 原始套装 ↔ 自制连帽衫+长裤(不同几何,绑定同一骨架)切换 | 截图02/03 |
|
||||
| 站/坐/阅读 | ✅ 坐姿 hips=0.515m 精确对齐椅面0.46m;书本距两手各0.095m | 截图04/05 + probe 数据 |
|
||||
| 环境↔角色解耦 | ✅ 昼夜+晴雨切换后 19骨四元数 maxDelta=0(程序断言) | `decouplingAssert: {pass:true}` |
|
||||
| 视线/表情交互 | ✅ 视线转向镜头时眼球偏转+happy表情权重0→0.9 | 截图07/07b/07c |
|
||||
| 标准动画文件播放 | ✅ test.vrma(VRMA 1.0)跨模型重定向播放 | 截图08/08b, 录屏 |
|
||||
| 真实天气接口 | ✅ Open-Meteo 无Key 取得3城市气温/降水/天气码/日出日落 + 中文地理编码 | `evidence/weather/*.json` |
|
||||
| 性能实测 | 67k三角/61 draw calls/27纹理/42MB JS堆;SwiftShader软渲染24fps(下界) | `runtime-stats.json` |
|
||||
|
||||
## 复现步骤
|
||||
|
||||
```bash
|
||||
cd experiments/hel-702-3d-verify
|
||||
npm install
|
||||
npx esbuild src/main.js --bundle --format=esm --outfile=public/bundle.js
|
||||
# 浏览器人工查看: 任一静态服务器指向本目录, 路由 / -> public/index.html, /assets/ -> assets/
|
||||
# 自动取证(截图+录屏+数据):
|
||||
node capture/capture.mjs # 完整序列(约90s, 需 playwright chromium)
|
||||
node capture/capture.mjs --shot 'state={"action":"read","time":"night","weather":"rain"}' cam=main out=x.png
|
||||
```
|
||||
|
||||
页面 URL 参数:`?model=twist|seed&cam=main|front|wide|closeup`。
|
||||
运行时控制台 API:`__app.setState({outfit:'A'|'B', action:'stand'|'sit'|'read'|'vrma', time:'day'|'night', weather:'sunny'|'rain', gaze:'idle'|'camera'})`,`__app.snapshotPose()` / `__app.assertPosePreserved()`(环境切换姿态保持断言),`__app.probe()`(骨骼/道具几何数据)。
|
||||
|
||||
## 素材与授权(详见 assets/manifest.json)
|
||||
|
||||
- `VRM1_Constraint_Twist_Sample.vrm` — pixiv Inc., VRM Public License 1.0(允许改変/再配布的公开授权,内嵌元数据可在运行时读取,本页已读取展示)。来源 vrm-c/vrm-specification 官方样例仓库,sha256 见 manifest。
|
||||
- `Seed-san.vrm` — VirtualCast, Inc., 同上。
|
||||
- `test.vrma` — three-vrm(MIT) 仓库示例动画。
|
||||
- 服装B(连帽衫+长裤)为本验证自制占位几何,只证明"同一骨架绑定两套不同服装并切换"的管线,正式服装属于后续美术施工。
|
||||
|
||||
## 关键设计(供正式施工沿用)
|
||||
|
||||
1. 环境状态(time/weather/灯光/雨)与角色状态(outfit/action/gaze)在代码层完全隔离,环境更新不触碰 `vrm` 对象 —— 天气刷新不会导致角色重建或姿势重置。
|
||||
2. 姿势走 VRM normalized 人形骨架(任何 VRM 通用),目标四元数 + 指数平滑 = 动作自然过渡;坐姿按家具实测高度计算 hips 偏移。
|
||||
3. 道具(书本)按帧放在两手骨骼中点并对齐角色朝向,不依赖特定模型手型。
|
||||
4. 服装=骨架上的独立网格集合:原生 VRoid 模型 Tops/Bottoms/Shoes 本就是独立 skinned primitive,切换=可见性/几何集合切换,骨架不动。
|
||||
5. 行为/心情接口预留:`__app.setState({gaze})` 即注意力接口雏形;八字/心情引擎接入点在 `applyState` 的角色段,本轮未接(明确标记:非真实状态引擎驱动)。
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"note": "HEL-702 素材清单:来源/哈希/授权(生成于 2026-10-20)",
|
||||
"assets": [
|
||||
{
|
||||
"file": "VRM1_Constraint_Twist_Sample.vrm",
|
||||
"sizeBytes": 10776032,
|
||||
"sizeMB": "10.3",
|
||||
"sha256": "12c2b97e95e700783a6a550dc0eee2d7880aeedccef9ae67bc4c5a2f0f2631a2",
|
||||
"source": "https://github.com/vrm-c/vrm-specification/tree/master/samples/VRM1_Constraint_Twist_Sample",
|
||||
"author": "pixiv Inc.",
|
||||
"license": "VRM Public License 1.0",
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"embeddedMeta": {
|
||||
"name": "VRM1_Constraint_Twist_Sample",
|
||||
"authors": ["pixiv Inc."],
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"commercialUssageName": null,
|
||||
"modificationName": null,
|
||||
"redistributionName": null
|
||||
},
|
||||
"structure": "3 skinned meshes (Body 5 prims / Face 7 prims / Hair), 83 skin joints, VRoid 材质分区: Tops_01_CLOTH / Bottoms_01_CLOTH / Shoes_01_CLOTH 独立 primitive, 19/19 标准人形骨骼齐全, 57 表情形态键"
|
||||
},
|
||||
{
|
||||
"file": "Seed-san.vrm",
|
||||
"sizeBytes": 10917800,
|
||||
"sizeMB": "10.4",
|
||||
"sha256": "624d0d554bc205bbdc33e22a68a2c3c20edebb3e573011ead8878a65e5329b23",
|
||||
"source": "https://github.com/vrm-c/vrm-specification/tree/master/samples/Seed-san",
|
||||
"author": "VirtualCast, Inc.",
|
||||
"license": "VRM Public License 1.0",
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"embeddedMeta": {
|
||||
"name": "Seed-san",
|
||||
"authors": ["VirtualCast, Inc."],
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/"
|
||||
},
|
||||
"structure": "5 skinned meshes, wear 网格 12 个材质 primitive, 23 skin joints, 43 形态键, PBR+MToon 混合(验证 MToon 兼容性用)"
|
||||
},
|
||||
{
|
||||
"file": "test.vrma",
|
||||
"sizeBytes": 11548,
|
||||
"sha256": "38d0fd12d61e896f1a970b5e358ebb41a96c8d5ee8e284496ea18f0ba1f04e7b",
|
||||
"source": "https://github.com/pixiv/three-vrm/tree/dev/packages/three-vrm-animation/examples/models",
|
||||
"author": "pixiv Inc. (three-vrm, MIT)",
|
||||
"license": "MIT (作为 three-vrm 仓库示例文件分发)",
|
||||
"content": "VRM Animation 1.0 示例:51 骨骼映射 + happy 表情 + lookAt 通道,3 动画通道(旋转/位移),用于验证标准动画文件加载与重定向"
|
||||
}
|
||||
],
|
||||
"runtimeDeps": {
|
||||
"three": "^0.170.0 (MIT)",
|
||||
"@pixiv/three-vrm": "^3.4.2 (MIT)",
|
||||
"@pixiv/three-vrm-animation": "^3.4.2 (MIT)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* HEL-702 证据采集:headless Chromium (SwiftShader) 驱动验证页,截图 + 录屏 + 运行数据 */
|
||||
import { chromium } from 'playwright';
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
||||
const PUB = path.join(ROOT, 'public');
|
||||
const PORT = 8931;
|
||||
|
||||
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.vrm': 'model/gltf-binary', '.vrma': 'model/gltf-binary', '.png': 'image/png' };
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = req.url.split('?')[0];
|
||||
let p;
|
||||
if (url === '/' || url === '/index.html') p = path.join(ROOT, 'public', 'index.html');
|
||||
else if (url.startsWith('/assets/')) p = path.join(ROOT, url.slice(1));
|
||||
else if (url === '/bundle.js') p = path.join(ROOT, 'public', 'bundle.js');
|
||||
else { res.writeHead(404); res.end('nf'); return; }
|
||||
fs.readFile(p, (e, buf) => {
|
||||
if (e) { res.writeHead(404); res.end('nf'); return; }
|
||||
res.writeHead(200, { 'content-type': MIME[path.extname(p)] || 'application/octet-stream' });
|
||||
res.end(buf);
|
||||
});
|
||||
});
|
||||
await new Promise((r) => server.listen(PORT, r));
|
||||
|
||||
const SHOT = process.argv.includes('--shot'); // 单镜头模式: --shot state=json cam=name out=xx.png
|
||||
const arg = (k) => process.argv.slice(2).find((a) => a.startsWith(k + '='))?.slice(k.length + 1);
|
||||
|
||||
const outDir = path.join(ROOT, 'evidence', 'shots');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||
|
||||
async function session({ video = false } = {}) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 960, height: 540 },
|
||||
recordVideo: video ? { dir: path.join(ROOT, 'evidence', '_video'), size: { width: 960, height: 540 } } : undefined,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
page.on('console', (m) => { if (m.type() === 'error') console.error('[page err]', m.text()); });
|
||||
page.on('pageerror', (e) => console.error('[page throw]', e.message, (e.stack?.split('\n').slice(1, 8).join('\n') || '')));
|
||||
return { ctx, page };
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
if (SHOT) {
|
||||
const { ctx, page } = await session();
|
||||
const state = JSON.parse(arg('state') || '{}');
|
||||
const cam = arg('cam') || 'main';
|
||||
const model = arg('model') || 'twist';
|
||||
const out = path.join(outDir, arg('out') || 'shot.png');
|
||||
await page.goto(`http://localhost:${PORT}/?cam=${cam}&model=${model}`);
|
||||
await page.waitForFunction(() => window.__app?.ready === true, null, { timeout: 60000 });
|
||||
await sleep(600);
|
||||
await page.evaluate((s) => window.__app.setState(s), state);
|
||||
await sleep(arg('wait') ? parseInt(arg('wait')) : 1600);
|
||||
await page.screenshot({ path: out });
|
||||
const stats = await page.evaluate(() => JSON.parse(JSON.stringify(window.__stats)));
|
||||
const probe = await page.evaluate(() => window.__app.probe());
|
||||
console.log('shot saved:', out);
|
||||
console.log('probe:', JSON.stringify(probe));
|
||||
console.log('stats:', JSON.stringify({ fps: stats.fps, loadMs: stats.loadMs, info: stats.info, meta: stats.modelMeta }));
|
||||
await ctx.close();
|
||||
} else {
|
||||
/* 完整证据序列(含录屏) */
|
||||
const { ctx, page } = await session({ video: true });
|
||||
await page.goto(`http://localhost:${PORT}/?cam=main&model=twist`);
|
||||
await page.waitForFunction(() => window.__app?.ready === true, null, { timeout: 60000 });
|
||||
await sleep(1200);
|
||||
|
||||
const cap = async (name, state, wait = 1500, caption) => {
|
||||
await page.evaluate((c) => { const el = document.getElementById('caption'); el.textContent = c; el.style.opacity = 1; }, caption || '');
|
||||
await page.evaluate((s) => window.__app.setState(s), state);
|
||||
await sleep(wait);
|
||||
await page.screenshot({ path: path.join(outDir, name) });
|
||||
const probe = await page.evaluate(() => window.__app.probe());
|
||||
globalProbes[name] = probe;
|
||||
console.log('captured', name, JSON.stringify(probe));
|
||||
};
|
||||
const globalProbes = {};
|
||||
|
||||
await cap('01-meta-stand-day-sunny.png', { outfit: 'A', action: 'stand', time: 'day', weather: 'sunny', gaze: 'idle' }, 1200,
|
||||
'① VRM 实时加载 · 站立 · 白天/晴(HUD 显示模型内嵌授权元数据)');
|
||||
await cap('02-outfitA-front.png', { outfit: 'A', action: 'stand' }, 1500, '② 服装A:原始 Tops+Bottoms+Shoes');
|
||||
await page.evaluate(() => window.__app.snapshotPose());
|
||||
await cap('03-outfitB-front.png', { outfit: 'B', action: 'stand' }, 1800, '③ 服装B:自制连帽衫+长裤(不同几何,非改色)· 骨架未动');
|
||||
await cap('04-sit-day.png', { action: 'sit' }, 2000, '④ 坐下:hips 对齐椅面高度 0.46m,过渡平滑');
|
||||
await cap('05-read-day.png', { action: 'read', gaze: 'idle' }, 2000, '⑤ 阅读:书本绑定右手原始骨骼,视线落在书页');
|
||||
await page.evaluate(() => window.__app.snapshotPose());
|
||||
await cap('06-read-night-rain.png', { time: 'night', weather: 'rain' }, 2500, '⑥ 夜晚+雨:环境切换 · 姿态保持(HUD 显示断言结果)');
|
||||
const decoupling = await page.evaluate(() => window.__app.assertPosePreserved());
|
||||
console.log('decoupling assert:', JSON.stringify(decoupling));
|
||||
await page.screenshot({ path: path.join(outDir, '06b-decouple-assert.png') });
|
||||
await cap('07-gaze-camera.png', { gaze: 'camera' }, 1500, '⑦ 交互注意力:视线转向镜头 + happy 表情');
|
||||
await cap('08-vrma-play.png', { action: 'vrma', gaze: 'idle' }, 2500, '⑧ 播放标准 VRMA 动画文件(跨模型重定向)');
|
||||
await page.screenshot({ path: path.join(outDir, '08b-vrma-mid.png') });
|
||||
|
||||
/* 姿态保持 + 运行数据 */
|
||||
const stats = await page.evaluate(() => JSON.parse(JSON.stringify(window.__stats)));
|
||||
/* fps 稳态测量:连续采样 6s */
|
||||
const fps = await page.evaluate(async () => {
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
return { fps: window.__stats.fps, info: window.__stats.info, heapMB: window.__stats.heapMB ?? null };
|
||||
});
|
||||
stats.steady = fps;
|
||||
stats.decouplingAssert = decoupling;
|
||||
stats.userAgent = await page.evaluate(() => navigator.userAgent);
|
||||
stats.environment = 'headless chromium + SwiftShader software WebGL2 (无GPU沙箱的下界测量)';
|
||||
stats.probes = globalProbes;
|
||||
fs.writeFileSync(path.join(ROOT, 'evidence', 'runtime-stats.json'), JSON.stringify(stats, null, 2));
|
||||
console.log('stats saved');
|
||||
const video = page.video();
|
||||
const vidPath = await video.path();
|
||||
await ctx.close(); /* finalizes video */
|
||||
if (vidPath && fs.existsSync(vidPath)) {
|
||||
const dest = path.join(ROOT, 'evidence', 'hel-702-verify-demo.webm');
|
||||
fs.copyFileSync(vidPath, dest);
|
||||
console.log('video saved:', dest);
|
||||
}
|
||||
}
|
||||
|
||||
/* Seed-san 第二模型验证(管线通用性 + MToon/PBR 混合) */
|
||||
{
|
||||
const { ctx, page } = await session();
|
||||
await page.goto(`http://localhost:${PORT}/?cam=main&model=seed`);
|
||||
await page.waitForFunction(() => window.__app?.ready === true, null, { timeout: 60000 });
|
||||
await sleep(1500);
|
||||
await page.evaluate(() => window.__app.setState({ outfit: 'A', action: 'stand' }));
|
||||
await sleep(1200);
|
||||
await page.screenshot({ path: path.join(outDir, '09-seedsan-load.png') });
|
||||
const meta = await page.evaluate(() => window.__stats.modelMeta);
|
||||
const load = await page.evaluate(() => window.__stats.loadMs);
|
||||
fs.writeFileSync(path.join(ROOT, 'evidence', 'seed-san-runtime.json'), JSON.stringify({ meta, loadMs: load }, null, 2));
|
||||
console.log('seed-san ok:', JSON.stringify(meta), 'loadMs', load);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
server.close();
|
||||
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"modelMeta": {
|
||||
"name": "VRM1_Constraint_Twist_Sample",
|
||||
"authors": [
|
||||
"pixiv Inc."
|
||||
],
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"commercialUssageName": "(未设置→按VRM PL 1.0默认)",
|
||||
"modificationName": "(未设置→按VRM PL 1.0默认)",
|
||||
"allowedUserName": "(未设置)"
|
||||
},
|
||||
"asset": {
|
||||
"file": "VRM1_Constraint_Twist_Sample.vrm",
|
||||
"sizeBytes": 10776032,
|
||||
"sizeMB": "10.3",
|
||||
"sha256": "12c2b97e95e700783a6a550dc0eee2d7880aeedccef9ae67bc4c5a2f0f2631a2",
|
||||
"source": "https://github.com/vrm-c/vrm-specification/tree/master/samples/VRM1_Constraint_Twist_Sample",
|
||||
"author": "pixiv Inc.",
|
||||
"license": "VRM Public License 1.0",
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"embeddedMeta": {
|
||||
"name": "VRM1_Constraint_Twist_Sample",
|
||||
"authors": [
|
||||
"pixiv Inc."
|
||||
],
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"commercialUssageName": null,
|
||||
"modificationName": null,
|
||||
"redistributionName": null
|
||||
},
|
||||
"structure": "3 skinned meshes (Body 5 prims / Face 7 prims / Hair), 83 skin joints, VRoid 材质分区: Tops_01_CLOTH / Bottoms_01_CLOTH / Shoes_01_CLOTH 独立 primitive, 19/19 标准人形骨骼齐全, 57 表情形态键"
|
||||
},
|
||||
"loadMs": 152,
|
||||
"fps": 20,
|
||||
"info": {
|
||||
"calls": 61,
|
||||
"triangles": 67298,
|
||||
"textures": 27,
|
||||
"geometries": 58
|
||||
},
|
||||
"heapMB": 42,
|
||||
"steady": {
|
||||
"fps": 24,
|
||||
"info": {
|
||||
"calls": 61,
|
||||
"triangles": 67298,
|
||||
"textures": 27,
|
||||
"geometries": 58
|
||||
},
|
||||
"heapMB": 42
|
||||
},
|
||||
"decouplingAssert": {
|
||||
"pass": true,
|
||||
"maxDelta": 0
|
||||
},
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36",
|
||||
"environment": "headless chromium + SwiftShader software WebGL2 (无GPU沙箱的下界测量)",
|
||||
"probes": {
|
||||
"01-meta-stand-day-sunny.png": {
|
||||
"headY": 1.387,
|
||||
"hipsY": 0.908,
|
||||
"footMinY": 0.101,
|
||||
"kneeFwd": -0.043,
|
||||
"faceAngleToCamDeg": 10.9,
|
||||
"bookToRHand": null,
|
||||
"bookToLHand": null,
|
||||
"handDist": 0.475,
|
||||
"rHandPos": [
|
||||
-0.55,
|
||||
0.87,
|
||||
0.39
|
||||
],
|
||||
"lHandPos": [
|
||||
-0.12,
|
||||
0.87,
|
||||
0.18
|
||||
],
|
||||
"headPos": [
|
||||
-0.3,
|
||||
1.39,
|
||||
0.34
|
||||
],
|
||||
"bbox": [
|
||||
0.607,
|
||||
1.617,
|
||||
0.469
|
||||
],
|
||||
"eyeYaw": 491.3,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
-0.3,
|
||||
0,
|
||||
0.35
|
||||
],
|
||||
"rootYaw": 0.45,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"02-outfitA-front.png": {
|
||||
"headY": 1.386,
|
||||
"hipsY": 0.908,
|
||||
"footMinY": 0.101,
|
||||
"kneeFwd": -0.044,
|
||||
"faceAngleToCamDeg": 10.5,
|
||||
"bookToRHand": null,
|
||||
"bookToLHand": null,
|
||||
"handDist": 0.475,
|
||||
"rHandPos": [
|
||||
-0.54,
|
||||
0.87,
|
||||
0.39
|
||||
],
|
||||
"lHandPos": [
|
||||
-0.12,
|
||||
0.87,
|
||||
0.19
|
||||
],
|
||||
"headPos": [
|
||||
-0.31,
|
||||
1.39,
|
||||
0.33
|
||||
],
|
||||
"bbox": [
|
||||
0.607,
|
||||
1.617,
|
||||
0.469
|
||||
],
|
||||
"eyeYaw": 376.2,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
-0.3,
|
||||
0,
|
||||
0.35
|
||||
],
|
||||
"rootYaw": 0.45,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"03-outfitB-front.png": {
|
||||
"headY": 1.386,
|
||||
"hipsY": 0.908,
|
||||
"footMinY": 0.101,
|
||||
"kneeFwd": -0.044,
|
||||
"faceAngleToCamDeg": 10.5,
|
||||
"bookToRHand": null,
|
||||
"bookToLHand": null,
|
||||
"handDist": 0.475,
|
||||
"rHandPos": [
|
||||
-0.54,
|
||||
0.87,
|
||||
0.39
|
||||
],
|
||||
"lHandPos": [
|
||||
-0.11,
|
||||
0.87,
|
||||
0.19
|
||||
],
|
||||
"headPos": [
|
||||
-0.31,
|
||||
1.39,
|
||||
0.33
|
||||
],
|
||||
"bbox": [
|
||||
0.607,
|
||||
1.617,
|
||||
0.469
|
||||
],
|
||||
"eyeYaw": -362.9,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
-0.3,
|
||||
0,
|
||||
0.35
|
||||
],
|
||||
"rootYaw": 0.45,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"04-sit-day.png": {
|
||||
"headY": 0.995,
|
||||
"hipsY": 0.515,
|
||||
"footMinY": 0.01,
|
||||
"kneeFwd": 0.353,
|
||||
"faceAngleToCamDeg": 34.8,
|
||||
"bookToRHand": null,
|
||||
"bookToLHand": null,
|
||||
"handDist": 0.475,
|
||||
"rHandPos": [
|
||||
0.41,
|
||||
0.48,
|
||||
-0.04
|
||||
],
|
||||
"lHandPos": [
|
||||
0.87,
|
||||
0.48,
|
||||
0.1
|
||||
],
|
||||
"headPos": [
|
||||
0.62,
|
||||
1,
|
||||
0.11
|
||||
],
|
||||
"bbox": [
|
||||
0.603,
|
||||
1.617,
|
||||
0.666
|
||||
],
|
||||
"eyeYaw": -1164,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
0.62,
|
||||
0,
|
||||
0.1
|
||||
],
|
||||
"rootYaw": -0.3,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"05-read-day.png": {
|
||||
"headY": 0.989,
|
||||
"hipsY": 0.515,
|
||||
"footMinY": 0.01,
|
||||
"kneeFwd": 0.353,
|
||||
"faceAngleToCamDeg": 43.4,
|
||||
"bookToRHand": 0.095,
|
||||
"bookToLHand": 0.095,
|
||||
"handDist": 0.186,
|
||||
"rHandPos": [
|
||||
0.42,
|
||||
0.88,
|
||||
0.44
|
||||
],
|
||||
"lHandPos": [
|
||||
0.59,
|
||||
0.88,
|
||||
0.5
|
||||
],
|
||||
"headPos": [
|
||||
0.61,
|
||||
0.99,
|
||||
0.14
|
||||
],
|
||||
"bbox": [
|
||||
0.641,
|
||||
1.617,
|
||||
0.666
|
||||
],
|
||||
"eyeYaw": -1442.2,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
0.62,
|
||||
0,
|
||||
0.1
|
||||
],
|
||||
"rootYaw": -0.3,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"06-read-night-rain.png": {
|
||||
"headY": 0.989,
|
||||
"hipsY": 0.515,
|
||||
"footMinY": 0.01,
|
||||
"kneeFwd": 0.353,
|
||||
"faceAngleToCamDeg": 43.4,
|
||||
"bookToRHand": 0.095,
|
||||
"bookToLHand": 0.095,
|
||||
"handDist": 0.186,
|
||||
"rHandPos": [
|
||||
0.42,
|
||||
0.88,
|
||||
0.44
|
||||
],
|
||||
"lHandPos": [
|
||||
0.59,
|
||||
0.88,
|
||||
0.5
|
||||
],
|
||||
"headPos": [
|
||||
0.61,
|
||||
0.99,
|
||||
0.14
|
||||
],
|
||||
"bbox": [
|
||||
0.641,
|
||||
1.617,
|
||||
0.666
|
||||
],
|
||||
"eyeYaw": -838.3,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
0.62,
|
||||
0,
|
||||
0.1
|
||||
],
|
||||
"rootYaw": -0.3,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"07-gaze-camera.png": {
|
||||
"headY": 0.989,
|
||||
"hipsY": 0.515,
|
||||
"footMinY": 0.01,
|
||||
"kneeFwd": 0.353,
|
||||
"faceAngleToCamDeg": 43.4,
|
||||
"bookToRHand": 0.095,
|
||||
"bookToLHand": 0.095,
|
||||
"handDist": 0.186,
|
||||
"rHandPos": [
|
||||
0.42,
|
||||
0.88,
|
||||
0.44
|
||||
],
|
||||
"lHandPos": [
|
||||
0.59,
|
||||
0.88,
|
||||
0.5
|
||||
],
|
||||
"headPos": [
|
||||
0.61,
|
||||
0.99,
|
||||
0.14
|
||||
],
|
||||
"bbox": [
|
||||
0.641,
|
||||
1.617,
|
||||
0.666
|
||||
],
|
||||
"eyeYaw": 2191.6,
|
||||
"happyWeight": 0.9,
|
||||
"rootPos": [
|
||||
0.62,
|
||||
0,
|
||||
0.1
|
||||
],
|
||||
"rootYaw": -0.3,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
},
|
||||
"08-vrma-play.png": {
|
||||
"headY": 0.989,
|
||||
"hipsY": 0.515,
|
||||
"footMinY": 0.01,
|
||||
"kneeFwd": 0.346,
|
||||
"faceAngleToCamDeg": 38.3,
|
||||
"bookToRHand": null,
|
||||
"bookToLHand": null,
|
||||
"handDist": 0.186,
|
||||
"rHandPos": [
|
||||
-0.09,
|
||||
0.88,
|
||||
0.39
|
||||
],
|
||||
"lHandPos": [
|
||||
0.09,
|
||||
0.88,
|
||||
0.39
|
||||
],
|
||||
"headPos": [
|
||||
0,
|
||||
0.99,
|
||||
0.04
|
||||
],
|
||||
"bbox": [
|
||||
0.597,
|
||||
1.617,
|
||||
0.598
|
||||
],
|
||||
"eyeYaw": 898.1,
|
||||
"happyWeight": 0,
|
||||
"rootPos": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rootYaw": 0,
|
||||
"seatY": 0.46,
|
||||
"outfitBBound": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Seed-san",
|
||||
"authors": [
|
||||
"VirtualCast, Inc."
|
||||
],
|
||||
"licenseUrl": "https://vrm.dev/licenses/1.0/",
|
||||
"commercialUssageName": "(未设置→按VRM PL 1.0默认)",
|
||||
"modificationName": "(未设置→按VRM PL 1.0默认)",
|
||||
"allowedUserName": "(未设置)"
|
||||
},
|
||||
"loadMs": 121
|
||||
}
|
||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1 @@
|
||||
{"results":[{"id":1808926,"name":"杭州","latitude":30.29365,"longitude":120.16142,"elevation":12.0,"feature_code":"PPLA","country_code":"CN","admin1_id":1784764,"admin2_id":1808925,"timezone":"Asia/Shanghai","population":11936010,"country_id":1814991,"country":"中国","admin1":"浙江","admin2":"杭州市"},{"id":6976333,"name":"杭州","latitude":30.06517,"longitude":102.19527,"elevation":2321.0,"feature_code":"PPL","country_code":"CN","admin1_id":1794299,"admin2_id":1810269,"timezone":"Asia/Shanghai","country_id":1814991,"country":"中国","admin1":"四川","admin2":"甘孜藏族自治州"}],"generationtime_ms":0.11372566}
|
||||
@@ -0,0 +1 @@
|
||||
{"latitude":31.247803,"longitude":121.5,"generationtime_ms":0.2837181091308594,"utc_offset_seconds":28800,"timezone":"Asia/Shanghai","timezone_abbreviation":"GMT+8","elevation":3.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","relative_humidity_2m":"%","precipitation":"mm","weather_code":"wmo code","wind_speed_10m":"km/h"},"current":{"time":"2026-10-20T09:45","interval":900,"temperature_2m":25.4,"relative_humidity_2m":79,"precipitation":0.00,"weather_code":3,"wind_speed_10m":5.4},"daily_units":{"time":"iso8601","sunrise":"iso8601","sunset":"iso8601","temperature_2m_max":"°C","temperature_2m_min":"°C","precipitation_sum":"mm"},"daily":{"time":["2026-10-20","2026-10-21"],"sunrise":["2026-10-20T06:00","2026-10-21T06:00"],"sunset":["2026-10-20T17:17","2026-10-21T17:16"],"temperature_2m_max":[27.6,22.0],"temperature_2m_min":[21.4,15.5],"precipitation_sum":[2.60,8.90]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"latitude":35.7,"longitude":139.6875,"generationtime_ms":0.12254714965820312,"utc_offset_seconds":32400,"timezone":"Asia/Tokyo","timezone_abbreviation":"GMT+9","elevation":43.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","relative_humidity_2m":"%","precipitation":"mm","weather_code":"wmo code","wind_speed_10m":"km/h"},"current":{"time":"2026-10-20T10:45","interval":900,"temperature_2m":24.2,"relative_humidity_2m":84,"precipitation":0.00,"weather_code":2,"wind_speed_10m":7.0},"daily_units":{"time":"iso8601","sunrise":"iso8601","sunset":"iso8601","temperature_2m_max":"°C","temperature_2m_min":"°C","precipitation_sum":"mm"},"daily":{"time":["2026-10-20","2026-10-21"],"sunrise":["2026-10-20T05:51","2026-10-21T05:52"],"sunset":["2026-10-20T16:59","2026-10-21T16:58"],"temperature_2m_max":[25.2,28.0],"temperature_2m_min":[16.4,18.4],"precipitation_sum":[0.60,1.00]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"latitude":30.263618,"longitude":120.14051,"generationtime_ms":0.2598762512207031,"utc_offset_seconds":28800,"timezone":"Asia/Shanghai","timezone_abbreviation":"GMT+8","elevation":13.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","relative_humidity_2m":"%","precipitation":"mm","weather_code":"wmo code","wind_speed_10m":"km/h"},"current":{"time":"2026-10-20T09:45","interval":900,"temperature_2m":24.8,"relative_humidity_2m":79,"precipitation":0.00,"weather_code":3,"wind_speed_10m":2.0},"daily_units":{"time":"iso8601","sunrise":"iso8601","sunset":"iso8601","temperature_2m_max":"°C","temperature_2m_min":"°C","precipitation_sum":"mm"},"daily":{"time":["2026-10-20","2026-10-21"],"sunrise":["2026-10-20T06:04","2026-10-21T06:05"],"sunset":["2026-10-20T17:23","2026-10-21T17:22"],"temperature_2m_max":[29.4,21.4],"temperature_2m_min":[20.7,14.8],"precipitation_sum":[2.60,4.70]}}
|
||||
@@ -0,0 +1,666 @@
|
||||
{
|
||||
"name": "hel-702-3d-verify",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hel-702-3d-verify",
|
||||
"dependencies": {
|
||||
"@pixiv/three-vrm": "^3.4.2",
|
||||
"@pixiv/three-vrm-animation": "^3.4.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"playwright": "^1.49.0",
|
||||
"three": "^0.170.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm/-/three-vrm-3.5.5.tgz",
|
||||
"integrity": "sha512-RPXy7jYAXs704NIpZlosB0U2ENu21G9DrqGWdQgRe8dShaCo1ugpj+6BVPRCy91nt+MPMA96j5rbsSzEl0HlQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/three-vrm-core": "3.5.5",
|
||||
"@pixiv/three-vrm-materials-hdr-emissive-multiplier": "3.5.5",
|
||||
"@pixiv/three-vrm-materials-mtoon": "3.5.5",
|
||||
"@pixiv/three-vrm-materials-v0compat": "3.5.5",
|
||||
"@pixiv/three-vrm-node-constraint": "3.5.5",
|
||||
"@pixiv/three-vrm-springbone": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-animation": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-animation/-/three-vrm-animation-3.5.5.tgz",
|
||||
"integrity": "sha512-Q0kEtmftxfv0iCUHQkJH2jAr1zmT9ywYXnAzhxnLPMYCsx0Kken3J3U2GJF9PdGZmLlKpoK0LFEXVdD/L7ZA4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/three-vrm-core": "3.5.5",
|
||||
"@pixiv/types-vrmc-vrm-1.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-vrm-animation-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-core": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-core/-/three-vrm-core-3.5.5.tgz",
|
||||
"integrity": "sha512-jjaXZbMVRsk2HAGVxqDLmSJyGOJqbMcJpuTHR1TY/TN1FxiKdAzSqdr/ZUcczYipNf8IL9vDT9NX2XmWj+ybRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrm-0.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-vrm-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-materials-hdr-emissive-multiplier": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-hdr-emissive-multiplier/-/three-vrm-materials-hdr-emissive-multiplier-3.5.5.tgz",
|
||||
"integrity": "sha512-XnHEqpY4ctYMucpeRcptdphOe0RrW7V5fUvojasD8Ja4MTjjrjCNFxsAvp3+HBkPZbkTM2sDCjJBJNzxN1scrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-materials-mtoon": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-mtoon/-/three-vrm-materials-mtoon-3.5.5.tgz",
|
||||
"integrity": "sha512-7lThHf4OKBOsVenD+R6L6HffNUlKO3Twnhr3+z77MnUsSzsOanjVKJtjSIly9QJ6hgl8YJmI6RnxCKqXcpplog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrm-0.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-materials-v0compat": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-v0compat/-/three-vrm-materials-v0compat-3.5.5.tgz",
|
||||
"integrity": "sha512-NjsTK4P4qdcr+RoL2gEUNgF2CY/aplNvyHHxI99N7D6CxDZ2H93FQ9aIuLCABj5b+4CIvldWMCcnS7YuLIr3Lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrm-0.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-node-constraint": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-node-constraint/-/three-vrm-node-constraint-3.5.5.tgz",
|
||||
"integrity": "sha512-+ShEXioZ5Ctf0EZ5ZViZ4n0jhV/qtJRH0Juw8nRnMNODZXDSr+B3An2lrfRoMhowCzG63GiUG4lXWV9noMXosA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrmc-node-constraint-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/three-vrm-springbone": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-springbone/-/three-vrm-springbone-3.5.5.tgz",
|
||||
"integrity": "sha512-GWAWrIegGH/u1ZA+Es03Qzc2yafrQOrhtal9jzUIAgU8Wm2DYuqAABOY+du/9RpBRK8D4T8DBPvRGQqvfrQ/uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrm-0.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-springbone-1.0": "3.5.5",
|
||||
"@pixiv/types-vrmc-springbone-extended-collider-1.0": "3.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixiv/types-vrm-0.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrm-0.0/-/types-vrm-0.0-3.5.5.tgz",
|
||||
"integrity": "sha512-2S/0jswMjJTwL9ky8YzDR0CIaP6ZmE+HFuyW/0h+SA+awELf+J7WGgC8lTLM1MXzokO7Z2YVljU2F0fEhlKDIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0/-/types-vrmc-materials-hdr-emissive-multiplier-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-IuHw9ehXt2bymqfdFV+f2cCXo8kvJUfh0Zuut8qmKXoEb7HVyLGIQyS4TcQHG5yY22UTWHI1Axo5tQv35lPTgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-materials-mtoon-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-materials-mtoon-1.0/-/types-vrmc-materials-mtoon-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-GFVuksAJVhrWTXYmI/ucNsE611/d8N6BoZrzhUAAINndt98loyzapW2+DJu+GsUBO5yxd+vLsL0FB16XD9+TGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-node-constraint-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-node-constraint-1.0/-/types-vrmc-node-constraint-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-8EbJ1uzkn9yJQarCChmpG12+mc2c7PimHvQ8H6/tMh/hqp5NtJND0z2lR2KeVxw9A3M+TpF+xgImhLBxGjqwzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-springbone-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-1.0/-/types-vrmc-springbone-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-gRQ5Y7PaGiMefvOrJ3hvoSPqpA67jiwyhwh8ItjsPi3yri/Hn3CeXplW3Cbx3ZPbfu+C3Q2MCRa2TqAUOjeMRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-springbone-extended-collider-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-extended-collider-1.0/-/types-vrmc-springbone-extended-collider-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-+6RxLaAvVguP7upjS/OT4viNPWvac1HBRQY1nmdvfYNQuCMdrMsCZqvy2af0Fcjnda4V9JbKFIpZDC0lrw1+uQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-vrm-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-1.0/-/types-vrmc-vrm-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-WnemHHo2375BcBdumEhvrupZzsMSzTfdWlokmV9sUX2oC+OAQqJFfBjLF8ppkOhzHpl4MAPJqEfuhBJtNPm9ww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixiv/types-vrmc-vrm-animation-1.0": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-animation-1.0/-/types-vrmc-vrm-animation-1.0-3.5.5.tgz",
|
||||
"integrity": "sha512-pyow6wBLdRZbLQbtHqu8uvYG+NXJbwIiLVK6pM6UvtaYotecOv43t6qIdMLYuieV+F+bEJcdfOLFZZVolrOOcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixiv/types-vrmc-vrm-1.0": "3.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.64.1.tgz",
|
||||
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.170.0",
|
||||
"resolved": "https://registry.npmmirror.com/three/-/three-0.170.0.tgz",
|
||||
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "hel-702-3d-verify",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.js --bundle --format=esm --outfile=public/bundle.js",
|
||||
"serve": "npx -y serve public -l 8931",
|
||||
"capture": "node capture/capture.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixiv/three-vrm": "^3.4.2",
|
||||
"@pixiv/three-vrm-animation": "^3.4.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"playwright": "^1.49.0",
|
||||
"three": "^0.170.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HEL-702 · 独立3D角色验证</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; background: #1a1d24; }
|
||||
#app { position: fixed; inset: 0; }
|
||||
#hud {
|
||||
position: fixed; left: 12px; top: 12px; z-index: 10;
|
||||
font: 12px/1.55 ui-monospace, Menlo, Consolas, monospace;
|
||||
color: #e8e4da; background: rgba(18, 20, 28, 0.82);
|
||||
padding: 10px 14px; border-radius: 8px; max-width: 520px;
|
||||
border: 1px solid rgba(255,255,255,0.08); white-space: pre-wrap;
|
||||
}
|
||||
#caption {
|
||||
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 10;
|
||||
font: 15px/1.4 system-ui, sans-serif; color: #fff;
|
||||
background: rgba(0,0,0,0.55); padding: 8px 18px; border-radius: 999px;
|
||||
opacity: 0; transition: opacity .35s; pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="hud">加载中…</div>
|
||||
<div id="caption"></div>
|
||||
<script type="module" src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
function parseGLB(buf) {
|
||||
if (buf.readUInt32LE(0) !== 0x46546c67) throw new Error('not GLB');
|
||||
const jsonLen = buf.readUInt32LE(12);
|
||||
return JSON.parse(buf.slice(20, 20 + jsonLen).toString('utf8'));
|
||||
}
|
||||
|
||||
const WANT_BONES = new Set(['hips','spine','chest','upperChest','neck','head',
|
||||
'leftUpperLeg','leftLowerLeg','leftFoot','rightUpperLeg','rightLowerLeg','rightFoot',
|
||||
'leftUpperArm','leftLowerArm','leftHand','rightUpperArm','rightLowerArm','rightHand']);
|
||||
|
||||
for (const file of process.argv.slice(2)) {
|
||||
const buf = fs.readFileSync(file);
|
||||
const g = parseGLB(buf);
|
||||
const vrm0 = g.extensions?.VRM;
|
||||
const vrm1 = g.extensions?.VRMC_vrm;
|
||||
const out = { file, sizeBytes: buf.byteLength, gltfVersion: g.asset?.version, generator: g.asset?.generator };
|
||||
|
||||
if (vrm1) {
|
||||
const m = vrm1.meta;
|
||||
out.standard = 'VRM1.0';
|
||||
out.meta = {
|
||||
name: m?.name, authors: m?.authors, licenseUrl: m?.licenseUrl,
|
||||
commercialUssageName: m?.commercialUssageName, modificationName: m?.modificationName,
|
||||
allowPoliticalReligious: m?.allowPoliticalReligiousName, allowAntisocialHate: m?.allowAntisocialOrHateUsageName,
|
||||
creditNotation: m?.creditNotationName, redistribution: m?.redistributionName,
|
||||
modification: m?.modificationName, allowedUserName: m?.allowedUserName,
|
||||
references: m?.references, contactInformation: m?.contactInformation,
|
||||
};
|
||||
const hb = vrm1.humanoid?.humanBones ?? {};
|
||||
const have = new Set(Array.isArray(hb) ? hb.map((b) => b.bone) : Object.keys(hb));
|
||||
out.humanoidMissing = [...WANT_BONES].filter((b) => !have.has(b));
|
||||
out.humanoidCount = have.size;
|
||||
const ex = vrm1.expressions ?? {};
|
||||
out.expressions = (Array.isArray(ex) ? ex : Object.values(ex)).map((e) => e.name);
|
||||
} else if (vrm0) {
|
||||
out.standard = 'VRM0.x';
|
||||
const m = vrm0.meta;
|
||||
out.meta = {
|
||||
title: m?.title, author: m?.author,
|
||||
allowedUserName: m?.allowedUserName, commercialUssageName: m?.commercialUssageName,
|
||||
sexualUssageName: m?.sexualUssageName, violentUssageName: m?.violentUssageName,
|
||||
licenseName: m?.licenseName, licenseUrl: m?.licenseUrl, otherLicenseUrl: m?.otherLicenseUrl,
|
||||
};
|
||||
out.humanoidMissing = [];
|
||||
out.expressions = (vrm0.blendShapeMaster?.blendShapeGroups ?? []).map((b) => b.name);
|
||||
}
|
||||
|
||||
const nodeNames = (g.nodes ?? []).map((n) => n.name);
|
||||
out.nodes = g.nodes?.length ?? 0;
|
||||
out.meshes = (g.meshes ?? []).map((m) => ({
|
||||
name: m.name,
|
||||
primitives: m.primitives.length,
|
||||
vertices: m.primitives[0]?.attributes?.POSITION ? 'y' : '?',
|
||||
skinned: !!m.primitives[0]?.attributes?.JOINTS_0,
|
||||
targets: (m.primitives[0]?.targets ?? []).length,
|
||||
}));
|
||||
out.materials = (g.materials ?? []).map((m) => ({
|
||||
name: m.name,
|
||||
type: m.extensions?.VRMC_materials_mtoon ? 'MToon' : (m.extensions?.KHR_materials_unlit ? 'unlit' : 'pbr'),
|
||||
}));
|
||||
out.textures = g.images?.length ?? 0;
|
||||
out.imageFormats = [...new Set((g.images ?? []).map((i) => i.mimeType ?? i.uri?.split('.').pop() ?? '?'))];
|
||||
const joints = g.skins?.[0]?.joints?.length ?? 0;
|
||||
out.skinJoints = joints;
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
|
||||
import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation';
|
||||
|
||||
/* ============ HEL-702 独立3D角色验证页 ============
|
||||
* 环境状态(time/weather) 与 角色状态(outfit/action/gaze) 完全分离。
|
||||
* 天气/时间切换不触碰角色骨架 —— assertPosePreserved() 可程序化验证。
|
||||
* ================================================== */
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const MODEL = params.get('model') || 'twist';
|
||||
const modelFile = MODEL === 'seed' ? 'assets/Seed-san.vrm' : 'assets/VRM1_Constraint_Twist_Sample.vrm';
|
||||
|
||||
const app = {
|
||||
ready: false,
|
||||
state: { outfit: 'A', action: 'stand', time: 'day', weather: 'sunny', gaze: 'idle' },
|
||||
stats: {},
|
||||
notes: [],
|
||||
};
|
||||
|
||||
/* ---------- renderer / scene ---------- */
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
document.getElementById('app').appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 0.1, 50);
|
||||
const CAMS = {
|
||||
main: { pos: [1.35, 1.35, 2.6], look: [0, 0.9, 0] },
|
||||
front: { pos: [0, 1.15, 2.3], look: [0, 0.95, 0] },
|
||||
wide: { pos: [2.3, 1.7, 3.4], look: [0.1, 0.8, 0] },
|
||||
closeup: { pos: [0.75, 1.15, 1.15], look: [0.05, 1.05, 0.1] },
|
||||
};
|
||||
function setCam(name) {
|
||||
const c = CAMS[name] || CAMS.main;
|
||||
camera.position.set(...c.pos);
|
||||
camera.lookAt(...c.look);
|
||||
}
|
||||
setCam(params.get('cam') || 'main');
|
||||
|
||||
/* ---------- room ---------- */
|
||||
const room = new THREE.Group();
|
||||
scene.add(room);
|
||||
const matWall = new THREE.MeshStandardMaterial({ color: 0xcfc8bd, roughness: 0.95 });
|
||||
const matWall2 = new THREE.MeshStandardMaterial({ color: 0xb9b2a6, roughness: 0.95 });
|
||||
const matFloor = new THREE.MeshStandardMaterial({ color: 0x8a6a4e, roughness: 0.8 });
|
||||
const matWood = new THREE.MeshStandardMaterial({ color: 0xa07850, roughness: 0.7 });
|
||||
|
||||
const floor = new THREE.Mesh(new THREE.BoxGeometry(4.6, 0.08, 4.6), matFloor);
|
||||
floor.position.y = -0.04;
|
||||
floor.receiveShadow = true;
|
||||
room.add(floor);
|
||||
const wallBack = new THREE.Mesh(new THREE.BoxGeometry(4.6, 2.75, 0.08), matWall);
|
||||
wallBack.position.set(0, 1.375, -2.3);
|
||||
wallBack.receiveShadow = true;
|
||||
room.add(wallBack);
|
||||
const wallLeft = new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.75, 4.6), matWall2);
|
||||
wallLeft.position.set(-2.3, 1.375, 0);
|
||||
room.add(wallLeft);
|
||||
|
||||
/* window opening in back wall: 4 boxes around a hole (x: cx±x/2) */
|
||||
const WIN = { x: 0.62, y: 1.0, cx: 0.55, cy: 1.45 };
|
||||
function wallPiece(w, h, cx, cy) {
|
||||
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.08), matWall);
|
||||
m.position.set(cx, cy, -2.3);
|
||||
m.receiveShadow = true;
|
||||
room.add(m);
|
||||
}
|
||||
wallPiece(4.6, 2.75 - (WIN.cy + WIN.y / 2), 0, WIN.cy + WIN.y / 2 + (2.75 - (WIN.cy + WIN.y / 2)) / 2); // top
|
||||
wallPiece(4.6, WIN.cy - WIN.y / 2, 0, (WIN.cy - WIN.y / 2) / 2); // bottom
|
||||
wallPiece((WIN.cx - WIN.x / 2) - (-2.3), WIN.y, (-2.3 + (WIN.cx - WIN.x / 2)) / 2, WIN.cy); // left
|
||||
wallPiece(2.3 - (WIN.cx + WIN.x / 2), WIN.y, (2.3 + (WIN.cx + WIN.x / 2)) / 2, WIN.cy); // right
|
||||
/* window frame + glass */
|
||||
const frameMat = new THREE.MeshStandardMaterial({ color: 0xf0ece4, roughness: 0.5 });
|
||||
const fr1 = new THREE.Mesh(new THREE.BoxGeometry(WIN.x + 0.08, 0.05, 0.1), frameMat);
|
||||
fr1.position.set(WIN.cx, WIN.cy + WIN.y / 2, -2.28);
|
||||
const fr2 = fr1.clone(); fr2.position.y = WIN.cy - WIN.y / 2;
|
||||
const fr3 = new THREE.Mesh(new THREE.BoxGeometry(0.05, WIN.y + 0.08, 0.1), frameMat);
|
||||
fr3.position.set(WIN.cx - WIN.x / 2, WIN.cy, -2.28);
|
||||
const fr4 = fr3.clone(); fr4.position.x = WIN.cx + WIN.x / 2;
|
||||
const fr5 = new THREE.Mesh(new THREE.BoxGeometry(0.03, WIN.y, 0.09), frameMat);
|
||||
fr5.position.set(WIN.cx, WIN.cy, -2.28);
|
||||
room.add(fr1, fr2, fr3, fr4, fr5);
|
||||
const skyMat = new THREE.MeshBasicMaterial({ color: 0xbcd7f0 });
|
||||
const sky = new THREE.Mesh(new THREE.PlaneGeometry(7, 5), skyMat);
|
||||
sky.position.set(WIN.cx, WIN.cy, -2.65);
|
||||
room.add(sky);
|
||||
/* distant city silhouette (dark boxes outside window) */
|
||||
const cityMat = new THREE.MeshBasicMaterial({ color: 0x6f7f96 });
|
||||
const city = new THREE.Group();
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const h = 0.5 + Math.abs(Math.sin(i * 2.7)) * 1.1;
|
||||
const b = new THREE.Mesh(new THREE.BoxGeometry(0.42, h, 0.2), cityMat);
|
||||
b.position.set(WIN.cx - 1.6 + i * 0.42, h / 2 - 0.4, -2.5);
|
||||
city.add(b);
|
||||
}
|
||||
room.add(city);
|
||||
|
||||
/* rug */
|
||||
const rug = new THREE.Mesh(new THREE.CircleGeometry(0.85, 40), new THREE.MeshStandardMaterial({ color: 0x9c4f3f, roughness: 1 }));
|
||||
rug.rotation.x = -Math.PI / 2;
|
||||
rug.position.set(-0.55, 0.005, 0.35);
|
||||
rug.receiveShadow = true;
|
||||
room.add(rug);
|
||||
|
||||
/* desk + chair + lamp (椅面高 0.46m,坐姿按它计算) */
|
||||
const chair = new THREE.Group();
|
||||
const seatY = 0.46;
|
||||
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.05, 0.44), matWood);
|
||||
seat.position.y = seatY - 0.025;
|
||||
seat.castShadow = true;
|
||||
const backrest = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.5, 0.05), matWood);
|
||||
backrest.position.set(0, seatY + 0.25, -0.2);
|
||||
backrest.castShadow = true;
|
||||
chair.add(seat, backrest);
|
||||
for (const [x, z] of [[-0.18, -0.18], [0.18, -0.18], [-0.18, 0.18], [0.18, 0.18]]) {
|
||||
const leg = new THREE.Mesh(new THREE.BoxGeometry(0.05, seatY - 0.05, 0.05), matWood);
|
||||
leg.position.set(x, (seatY - 0.05) / 2, z);
|
||||
chair.add(leg);
|
||||
}
|
||||
chair.position.set(0.62, 0, 0.1);
|
||||
chair.rotation.y = -0.5;
|
||||
room.add(chair);
|
||||
|
||||
const desk = new THREE.Group();
|
||||
const top = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.05, 0.55), matWood);
|
||||
top.position.y = 0.74;
|
||||
top.castShadow = true;
|
||||
desk.add(top);
|
||||
for (const [x, z] of [[-0.5, -0.22], [0.5, -0.22], [-0.5, 0.22], [0.5, 0.22]]) {
|
||||
const leg = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.74, 0.05), matWood);
|
||||
leg.position.set(x, 0.37, z);
|
||||
desk.add(leg);
|
||||
}
|
||||
const mug = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 0.1, 16), new THREE.MeshStandardMaterial({ color: 0xd8cfc4, roughness: 0.4 }));
|
||||
mug.position.set(0.25, 0.815, 0.06);
|
||||
desk.add(mug);
|
||||
desk.position.set(-0.85, 0, -0.75);
|
||||
desk.rotation.y = 0.35;
|
||||
room.add(desk);
|
||||
|
||||
const lampG = new THREE.Group();
|
||||
const lampStand = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.03, 1.25, 10), new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.7, roughness: 0.4 }));
|
||||
lampStand.position.y = 0.625;
|
||||
const lampShade = new THREE.Mesh(new THREE.ConeGeometry(0.14, 0.18, 16, 1, true), new THREE.MeshStandardMaterial({ color: 0xf5e6c8, roughness: 0.6, side: THREE.DoubleSide }));
|
||||
lampShade.position.y = 1.3;
|
||||
lampG.add(lampStand, lampShade);
|
||||
lampG.position.set(1.55, 0, -1.9);
|
||||
room.add(lampG);
|
||||
|
||||
/* book prop (read 动作时挂到右手) */
|
||||
const bookG = new THREE.Group();
|
||||
const bookCover = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.015, 0.22), new THREE.MeshStandardMaterial({ color: 0x8a3033, roughness: 0.6 }));
|
||||
const bookPage = new THREE.Mesh(new THREE.BoxGeometry(0.145, 0.012, 0.205), new THREE.MeshStandardMaterial({ color: 0xf2ecd9, roughness: 0.9 }));
|
||||
bookPage.position.y = 0.008;
|
||||
bookG.add(bookCover, bookPage);
|
||||
bookG.visible = false;
|
||||
|
||||
/* ---------- lights ---------- */
|
||||
const hemi = new THREE.HemisphereLight(0xdfe8f5, 0x5a4a3a, 0.55);
|
||||
scene.add(hemi);
|
||||
const sun = new THREE.DirectionalLight(0xfff2dd, 2.0);
|
||||
sun.position.set(3, 4, 1.5);
|
||||
sun.castShadow = true;
|
||||
sun.shadow.mapSize.set(1024, 1024);
|
||||
sun.shadow.camera.left = -3; sun.shadow.camera.right = 3;
|
||||
sun.shadow.camera.top = 3; sun.shadow.camera.bottom = -3;
|
||||
scene.add(sun);
|
||||
const lampLight = new THREE.PointLight(0xffc98a, 0, 4, 2);
|
||||
lampLight.position.set(1.55, 1.22, -1.9);
|
||||
scene.add(lampLight);
|
||||
|
||||
/* rain (outside window) */
|
||||
const RAIN_N = 500;
|
||||
const rainGeo = new THREE.BufferGeometry();
|
||||
const rainPos = new Float32Array(RAIN_N * 3);
|
||||
for (let i = 0; i < RAIN_N; i++) {
|
||||
rainPos[i * 3] = WIN.cx - 1.8 + Math.random() * 3.6;
|
||||
rainPos[i * 3 + 1] = Math.random() * 4;
|
||||
rainPos[i * 3 + 2] = -2.5 - Math.random() * 0.6;
|
||||
}
|
||||
rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPos, 3));
|
||||
const rain = new THREE.Points(rainGeo, new THREE.PointsMaterial({ color: 0xaec6e8, size: 0.022, transparent: true, opacity: 0.85 }));
|
||||
rain.visible = false;
|
||||
scene.add(rain);
|
||||
|
||||
/* ---------- VRM character ---------- */
|
||||
let vrm = null;
|
||||
let mixer = null;
|
||||
let vrmaAction = null;
|
||||
let vrmaClipLen = 0;
|
||||
const rawBone = (n) => vrm?.humanoid.getRawBoneNode(n) ?? null;
|
||||
const normBone = (n) => vrm?.humanoid.getNormalizedBoneNode(n) ?? null;
|
||||
|
||||
/* outfit B: 自制占位服装(连帽衫+长裤,硬segment绑定原始骨骼) */
|
||||
const outfitB = new THREE.Group();
|
||||
const matPants = new THREE.MeshStandardMaterial({ color: 0x46618f, roughness: 0.85 });
|
||||
const matHood = new THREE.MeshStandardMaterial({ color: 0x9aa3ad, roughness: 0.9 });
|
||||
function seg(mesh, bone, pos) { mesh.position.copy(pos); mesh.castShadow = true; outfitB.add(mesh); mesh.userData.bone = bone; return mesh; }
|
||||
const B = (g, m) => new THREE.Mesh(g, m);
|
||||
seg(B(new THREE.BoxGeometry(0.34, 0.34, 0.2), matHood), 'chest', new THREE.Vector3(0, 0.02, 0)); // 衣身
|
||||
seg(B(new THREE.BoxGeometry(0.14, 0.12, 0.14), matHood), 'neck', new THREE.Vector3(0, 0.08, -0.03)); // 领口
|
||||
seg(B(new THREE.CylinderGeometry(0.055, 0.05, 0.26, 8), matHood), 'leftUpperArm', new THREE.Vector3(0, -0.1, 0)); // 袖L上
|
||||
seg(B(new THREE.CylinderGeometry(0.05, 0.045, 0.24, 8), matHood), 'leftLowerArm', new THREE.Vector3(0, -0.1, 0)); // 袖L下
|
||||
seg(B(new THREE.CylinderGeometry(0.055, 0.05, 0.26, 8), matHood), 'rightUpperArm', new THREE.Vector3(0, -0.1, 0)); // 袖R上
|
||||
seg(B(new THREE.CylinderGeometry(0.05, 0.045, 0.24, 8), matHood), 'rightLowerArm', new THREE.Vector3(0, -0.1, 0)); // 袖R下
|
||||
for (const side of ['left', 'right']) {
|
||||
seg(B(new THREE.CylinderGeometry(0.085, 0.075, 0.3, 10), matPants), side + 'UpperLeg', new THREE.Vector3(0, -0.15, 0)); // 裤大腿
|
||||
seg(B(new THREE.CylinderGeometry(0.07, 0.06, 0.3, 10), matPants), side + 'LowerLeg', new THREE.Vector3(0, -0.14, 0)); // 裤小腿
|
||||
}
|
||||
outfitB.visible = false;
|
||||
scene.add(outfitB);
|
||||
|
||||
/* ---------- 姿势系统(normalized 骨架,目标角度平滑过渡) ---------- */
|
||||
const POSES = {
|
||||
stand: {
|
||||
hips: { pos: null },
|
||||
leftShoulder: { z: 0.04 }, rightShoulder: { z: -0.04 },
|
||||
leftUpperArm: { z: -1.22, x: 0.1 }, rightUpperArm: { z: 1.22, x: 0.1 },
|
||||
leftLowerArm: { z: -0.18 }, rightLowerArm: { z: 0.18 },
|
||||
spine: { x: 0.02 }, neck: { x: 0.03 },
|
||||
},
|
||||
sit: {
|
||||
leftUpperLeg: { x: -1.45 }, rightUpperLeg: { x: -1.45 },
|
||||
leftLowerLeg: { x: 1.42 }, rightLowerLeg: { x: 1.42 },
|
||||
leftFoot: { x: -0.02 }, rightFoot: { x: -0.02 },
|
||||
spine: { x: 0.09 }, neck: { x: -0.04 },
|
||||
},
|
||||
read: {
|
||||
leftUpperLeg: { x: -1.45 }, rightUpperLeg: { x: -1.45 },
|
||||
leftLowerLeg: { x: 1.42 }, rightLowerLeg: { x: 1.42 },
|
||||
leftFoot: { x: -0.02 }, rightFoot: { x: -0.02 },
|
||||
spine: { x: 0.14 }, neck: { x: 0.22 },
|
||||
leftShoulder: { x: -0.5 }, rightShoulder: { x: -0.5 },
|
||||
leftUpperArm: { z: -0.25, y: -1.05 }, rightUpperArm: { z: 0.25, y: 1.05 },
|
||||
leftLowerArm: { y: -1.1, z: -0.3 }, rightLowerArm: { y: 1.1, z: 0.3 },
|
||||
},
|
||||
};
|
||||
const poseTargets = {}; // bone -> {quat, posOffset}
|
||||
let sitOffsetY = 0;
|
||||
let baseHipY = 0;
|
||||
|
||||
function computeTargets(name) {
|
||||
const p = POSES[name] || POSES.stand;
|
||||
for (const k of Object.keys(poseTargets)) delete poseTargets[k];
|
||||
for (const [bone, rot] of Object.entries(p)) {
|
||||
const n = normBone(bone);
|
||||
if (!n) continue;
|
||||
const q = new THREE.Quaternion();
|
||||
if (rot.x) q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), rot.x));
|
||||
if (rot.y) q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), rot.y));
|
||||
if (rot.z) q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), rot.z));
|
||||
poseTargets[bone] = { quat: q };
|
||||
}
|
||||
if (name === 'sit' || name === 'read') {
|
||||
poseTargets.hips = { quat: new THREE.Quaternion(), posY: hipsRestY + (seatY + 0.055 - baseHipY) };
|
||||
} else {
|
||||
poseTargets.hips = { quat: new THREE.Quaternion(), posY: hipsRestY };
|
||||
}
|
||||
}
|
||||
|
||||
/* gaze */
|
||||
const lookTarget = new THREE.Object3D();
|
||||
scene.add(lookTarget);
|
||||
let gazeMode = 'idle';
|
||||
let gazeWanderT = 0;
|
||||
|
||||
/* ---------- HUD ---------- */
|
||||
const hud = document.getElementById('hud');
|
||||
function renderHUD() {
|
||||
const s = app.state;
|
||||
const i = renderer.info;
|
||||
hud.innerHTML = `
|
||||
<b>HEL-702 · 独立角色验证</b> · ${MODEL === 'seed' ? 'Seed-san' : 'VRM1_Constraint_Twist_Sample'}<br>
|
||||
状态: 衣=${s.outfit} 动作=${s.action} 时间=${s.time} 天气=${s.weather} 视线=${s.gaze}<br>
|
||||
draw=${i.render.calls} tri=${i.render.triangles.toLocaleString()} tex=${i.memory.textures} geo=${i.memory.geometries} fps=${app.stats.fps ?? '-'}<br>
|
||||
<span style="color:#888">${app.notes.length ? app.notes[app.notes.length - 1] : ''}</span>`;
|
||||
}
|
||||
|
||||
/* ---------- decoupling assertion ---------- */
|
||||
let poseSnapshot = null;
|
||||
function snapshotPose() {
|
||||
const names = ['hips', 'head', 'leftHand', 'rightHand', 'leftUpperLeg'];
|
||||
const out = {};
|
||||
for (const n of names) {
|
||||
const b = normBone(n);
|
||||
if (b) out[n] = b.quaternion.clone();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
app.assertPosePreserved = () => {
|
||||
if (!poseSnapshot) return { pass: null, reason: 'no snapshot' };
|
||||
let maxDelta = 0;
|
||||
for (const [n, q] of Object.entries(poseSnapshot)) {
|
||||
const b = normBone(n);
|
||||
if (b) maxDelta = Math.max(maxDelta, b.quaternion.angleTo(q));
|
||||
}
|
||||
const pass = maxDelta < 1e-4;
|
||||
app.notes.push(`环境切换后姿态偏移 = ${maxDelta.toExponential(2)} rad → ${pass ? 'PASS 未重置' : 'FAIL 被重置'}`);
|
||||
return { pass, maxDelta };
|
||||
};
|
||||
|
||||
/* ---------- state apply ---------- */
|
||||
function applyState(next) {
|
||||
Object.assign(app.state, next);
|
||||
const s = app.state;
|
||||
if (!vrm) return;
|
||||
|
||||
/* 环境: 完全不触碰 vrm 与姿势 */
|
||||
if (s.time === 'night') {
|
||||
sun.intensity = 0.04; hemi.intensity = 0.1;
|
||||
lampLight.intensity = 14; skyMat.color.set(0x0a1428); cityMat.color.set(0x1a2230);
|
||||
renderer.toneMappingExposure = 1.0;
|
||||
} else {
|
||||
sun.intensity = 2.0; hemi.intensity = 0.55;
|
||||
lampLight.intensity = 0; skyMat.color.set(0xbcd7f0); cityMat.color.set(0x6f7f96);
|
||||
renderer.toneMappingExposure = 1.0;
|
||||
}
|
||||
rain.visible = s.weather === 'rain';
|
||||
if (s.weather === 'rain') {
|
||||
if (s.time === 'night') { skyMat.color.set(0x0a1428); }
|
||||
else skyMat.color.set(0x93a8bd);
|
||||
}
|
||||
|
||||
/* 角色 */
|
||||
const garments = { Tops: false, Bottoms: false, Shoes: false };
|
||||
if (s.outfit === 'B') { garments.Tops = true; garments.Bottoms = true; }
|
||||
vrm.scene.traverse((o) => {
|
||||
if (o.isMesh) {
|
||||
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
||||
for (const m of mats) {
|
||||
if (/Tops/.test(m.name)) m.visible = !garments.Tops;
|
||||
if (/Bottoms/.test(m.name)) m.visible = !garments.Bottoms;
|
||||
}
|
||||
}
|
||||
});
|
||||
outfitB.visible = s.outfit === 'B';
|
||||
|
||||
if (s.action === 'vrma') {
|
||||
if (vrmaAction) { vrmaAction.play(); computeTargets('stand'); }
|
||||
} else {
|
||||
if (vrmaAction) vrmaAction.stop();
|
||||
computeTargets(s.action);
|
||||
}
|
||||
rootTarget = ROOT_T[s.action] || ROOT_T.stand;
|
||||
bookG.visible = s.action === 'read';
|
||||
gazeMode = s.gaze;
|
||||
if (vrm.lookAt) vrm.lookAt.target = s.gaze === 'camera' ? camera : lookTarget;
|
||||
if (vrm.expressionManager) vrm.expressionManager.setValue('happy', s.gaze === 'camera' ? 0.9 : 0);
|
||||
renderHUD();
|
||||
}
|
||||
app.setState = (next) => { if (poseSnapshotForNext(next)) poseSnapshot = snapshotPose(); applyState(next); };
|
||||
function poseSnapshotForNext(next) {
|
||||
return ('time' in next) || ('weather' in next);
|
||||
}
|
||||
app.snapshotPose = () => { poseSnapshot = snapshotPose(); app.notes.push('已记录当前姿态快照'); renderHUD(); };
|
||||
|
||||
/* ---------- load ---------- */
|
||||
const t0 = performance.now();
|
||||
const loader = new GLTFLoader();
|
||||
loader.register((p) => new VRMLoaderPlugin(p));
|
||||
loader.register((p) => new VRMAnimationLoaderPlugin(p));
|
||||
const [gltf, gltfA, manifest] = await Promise.all([
|
||||
loader.loadAsync(modelFile),
|
||||
loader.loadAsync('assets/test.vrma'),
|
||||
fetch('assets/manifest.json').then((r) => r.json()),
|
||||
]);
|
||||
const loadMs = Math.round(performance.now() - t0);
|
||||
vrm = gltf.userData.vrm;
|
||||
if (!vrm) {
|
||||
console.error('[load] userData keys:', Object.keys(gltf.userData), 'extensions:', gltf.parser?.json?.extensionsUsed, gltf.parser?.json?.extensions);
|
||||
throw new Error('VRM plugin did not produce vrm from ' + modelFile);
|
||||
}
|
||||
VRMUtils.removeUnnecessaryVertices(gltf.scene);
|
||||
VRMUtils.combineSkeletons?.(gltf.scene);
|
||||
vrm.scene.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.frustumCulled = false; } });
|
||||
/* VRM1 模型面朝 +Z;镜头在 +Z 侧,不旋转 */
|
||||
scene.add(vrm.scene);
|
||||
if (vrm.lookAt) vrm.lookAt.target = lookTarget;
|
||||
|
||||
/* meta (embedded license info, read at runtime) */
|
||||
const m = vrm.meta;
|
||||
app.stats.modelMeta = {
|
||||
name: m.name, authors: m.authors, licenseUrl: m.licenseUrl,
|
||||
commercialUssageName: m.commercialUssageName ?? '(未设置→按VRM PL 1.0默认)',
|
||||
modificationName: m.modificationName ?? '(未设置→按VRM PL 1.0默认)',
|
||||
allowedUserName: m.allowedUserName ?? '(未设置)',
|
||||
};
|
||||
const entry = manifest.assets.find((a) => (MODEL === 'seed' ? a.file.includes('Seed') : a.file.includes('Twist')));
|
||||
app.stats.asset = entry;
|
||||
app.stats.loadMs = loadMs;
|
||||
|
||||
/* VRMA animation */
|
||||
const vrmA = gltfA.userData.vrmAnimation;
|
||||
if (vrmA) {
|
||||
const clip = createVRMAnimationClip(vrmA, vrm);
|
||||
vrmaClipLen = clip.duration;
|
||||
mixer = new THREE.AnimationMixer(vrm.scene);
|
||||
vrmaAction = mixer.clipAction(clip);
|
||||
app.stats.vrma = { duration: clip.duration, tracks: clip.tracks.length, name: entry ? 'test.vrma' : 'test.vrma' };
|
||||
}
|
||||
|
||||
/* pose init */
|
||||
const hips = normBone('hips');
|
||||
const hipsRestY = hips ? hips.position.y : 0;
|
||||
baseHipY = hips ? hips.getWorldPosition(new THREE.Vector3()).y : 0.86;
|
||||
sitOffsetY = seatY + 0.055 - baseHipY;
|
||||
computeTargets('stand');
|
||||
|
||||
/* 书本:每帧放在两手之间并对齐角色朝向(阅读姿态可证据化:bookToHand 距离) */
|
||||
scene.add(bookG);
|
||||
const _rH = new THREE.Vector3(); const _lH = new THREE.Vector3(); const _mid = new THREE.Vector3();
|
||||
function placeBook() {
|
||||
const r = rawBone('rightHand'); const l = rawBone('leftHand');
|
||||
if (!r || !l) return;
|
||||
r.getWorldPosition(_rH); l.getWorldPosition(_lH);
|
||||
_mid.copy(_rH).add(_lH).multiplyScalar(0.5);
|
||||
bookG.position.copy(_mid).y += 0.02;
|
||||
bookG.rotation.set(-1.35, vrm.scene.rotation.y, 0.1, 'YXZ');
|
||||
}
|
||||
|
||||
/* runtime bone-binding of outfitB segments (snapshot: add() reparents) */
|
||||
for (const o of [...outfitB.children]) {
|
||||
if (o.userData.bone) {
|
||||
const b = rawBone(o.userData.bone);
|
||||
if (b) b.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
/* 根节点运动目标(站立在地毯;坐下/阅读在椅子上) */
|
||||
const ROOT_T = {
|
||||
stand: { pos: new THREE.Vector3(-0.3, 0, 0.35), yaw: 0.45 },
|
||||
sit: { pos: new THREE.Vector3(0.62, 0, 0.1), yaw: -0.3 },
|
||||
read: { pos: new THREE.Vector3(0.62, 0, 0.1), yaw: -0.3 },
|
||||
vrma: { pos: new THREE.Vector3(0, 0, 0), yaw: 0 },
|
||||
};
|
||||
let rootTarget = ROOT_T.stand;
|
||||
|
||||
app.probe = () => {
|
||||
const wp = (n) => { const b = normBone(n); return b ? b.getWorldPosition(new THREE.Vector3()) : null; };
|
||||
const head = wp('head'); const hipsP = wp('hips');
|
||||
const lFoot = wp('leftFoot'); const rFoot = wp('rightFoot');
|
||||
const lKnee = wp('leftLowerLeg'); const rHandW = rawBone('rightHand') ? rawBone('rightHand').getWorldPosition(new THREE.Vector3()) : null;
|
||||
const lHandW = rawBone('leftHand') ? rawBone('leftHand').getWorldPosition(new THREE.Vector3()) : null;
|
||||
const bookW = bookG.visible ? bookG.getWorldPosition(new THREE.Vector3()) : null;
|
||||
const faceDir = new THREE.Vector3();
|
||||
const headN = normBone('head');
|
||||
if (headN) headN.getWorldDirection(faceDir);
|
||||
const toCam = head ? camera.position.clone().sub(head).normalize() : new THREE.Vector3();
|
||||
return {
|
||||
headY: +head?.y?.toFixed(3), hipsY: +hipsP?.y?.toFixed(3),
|
||||
footMinY: +Math.min(lFoot?.y ?? 9, rFoot?.y ?? 9).toFixed(3),
|
||||
kneeFwd: lKnee && hipsP ? +(lKnee.z - hipsP.z).toFixed(3) : null,
|
||||
faceAngleToCamDeg: head ? +THREE.MathUtils.radToDeg(faceDir.angleTo(toCam)).toFixed(1) : null,
|
||||
bookToRHand: bookW && rHandW ? +bookW.distanceTo(rHandW).toFixed(3) : null,
|
||||
bookToLHand: bookW && lHandW ? +bookW.distanceTo(lHandW).toFixed(3) : null,
|
||||
handDist: rHandW && lHandW ? +rHandW.distanceTo(lHandW).toFixed(3) : null,
|
||||
rHandPos: rHandW ? rHandW.toArray().map((v) => +v.toFixed(2)) : null,
|
||||
lHandPos: lHandW ? lHandW.toArray().map((v) => +v.toFixed(2)) : null,
|
||||
headPos: head ? head.toArray().map((v) => +v.toFixed(2)) : null,
|
||||
bbox: (() => { const b = new THREE.Box3().setFromObject(vrm.scene); const sz = b.getSize(new THREE.Vector3()); return sz.toArray().map((v) => +v.toFixed(3)); })(),
|
||||
eyeYaw: (() => { const la = vrm.lookAt; return la ? +THREE.MathUtils.radToDeg(la.yaw).toFixed(1) : null; })(),
|
||||
happyWeight: vrm.expressionManager ? +vrm.expressionManager.getValue('happy').toFixed(2) : null,
|
||||
rootPos: vrm.scene.position.toArray().map((v) => +v.toFixed(2)),
|
||||
rootYaw: +vrm.scene.rotation.y.toFixed(2),
|
||||
seatY, outfitBBound: outfitB.children.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
app.ready = true;
|
||||
app.notes.push(`模型加载 ${loadMs}ms · ${entry ? (entry.sizeMB + 'MB') : ''} · 授权: ${m.licenseUrl}`);
|
||||
applyState({});
|
||||
|
||||
/* ---------- loop ---------- */
|
||||
const clock = new THREE.Clock();
|
||||
let fpsAcc = 0, fpsN = 0, fpsT = 0;
|
||||
function loop() {
|
||||
requestAnimationFrame(loop);
|
||||
const dt = Math.min(clock.getDelta(), 0.05);
|
||||
const t = clock.elapsedTime;
|
||||
|
||||
/* root motion: 站位↔椅子 平滑移动 */
|
||||
if (vrm) {
|
||||
const k = 1 - Math.exp(-dt * 3.2);
|
||||
vrm.scene.position.lerp(rootTarget.pos, k);
|
||||
let dy = rootTarget.yaw - vrm.scene.rotation.y;
|
||||
dy = Math.atan2(Math.sin(dy), Math.cos(dy));
|
||||
vrm.scene.rotation.y += dy * k;
|
||||
}
|
||||
|
||||
/* pose easing toward targets (动作自然过渡) */
|
||||
if (vrm && app.state.action !== 'vrma') {
|
||||
const k = 1 - Math.exp(-dt * 5.5);
|
||||
for (const [bone, tgt] of Object.entries(poseTargets)) {
|
||||
const n = normBone(bone);
|
||||
if (!n) continue;
|
||||
n.quaternion.slerp(tgt.quat, k);
|
||||
if (tgt.posY !== undefined && bone === 'hips') n.position.y = THREE.MathUtils.lerp(n.position.y, tgt.posY, k);
|
||||
}
|
||||
/* breathing + micro sway on stand */
|
||||
const chest = normBone('chest');
|
||||
if (chest && (app.state.action === 'stand')) {
|
||||
chest.rotation.x = 0.02 * Math.sin(t * 1.7);
|
||||
}
|
||||
}
|
||||
if (app.state.action === 'read') placeBook();
|
||||
if (mixer && app.state.action === 'vrma') mixer.update(dt);
|
||||
if (vrm) vrm.update(dt);
|
||||
|
||||
/* gaze wander */
|
||||
if (gazeMode === 'idle') {
|
||||
gazeWanderT += dt;
|
||||
lookTarget.position.set(0.35 * Math.sin(t * 0.31), 1.15 + 0.12 * Math.sin(t * 0.21), 1.3);
|
||||
} else {
|
||||
lookTarget.position.copy(camera.position);
|
||||
}
|
||||
|
||||
/* rain fall */
|
||||
if (rain.visible) {
|
||||
const pos = rain.geometry.attributes.position;
|
||||
for (let i = 0; i < RAIN_N; i++) {
|
||||
let y = pos.getY(i) - dt * (2.6 + (i % 7) * 0.2);
|
||||
if (y < 0) y = 4;
|
||||
pos.setY(i, y);
|
||||
}
|
||||
pos.needsUpdate = true;
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
fpsAcc += dt; fpsN++; fpsT += dt;
|
||||
if (fpsT > 0.5) {
|
||||
app.stats.fps = Math.round(fpsN / fpsAcc);
|
||||
fpsAcc = 0; fpsN = 0; fpsT = 0;
|
||||
app.stats.info = {
|
||||
calls: renderer.info.render.calls,
|
||||
triangles: renderer.info.render.triangles,
|
||||
textures: renderer.info.memory.textures,
|
||||
geometries: renderer.info.memory.geometries,
|
||||
};
|
||||
if (performance) {
|
||||
if (performance.memory) app.stats.heapMB = Math.round(performance.memory.usedJSHeapSize / 1e6);
|
||||
}
|
||||
renderHUD();
|
||||
}
|
||||
}
|
||||
loop();
|
||||
window.__app = app;
|
||||
window.__stats = app.stats;
|
||||