dsh-plugin-vrmpet3d 0.1.1 → 0.1.3
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 +1 -0
- package/assets/game/levels.json +2925 -0
- package/desktop/game-visibility.cjs +50 -0
- package/desktop/main.js +60 -3
- package/desktop/preload.js +11 -0
- package/lib/client.core.js +279 -41
- package/lib/client.js +144 -16
- package/lib/client.menu.js +17 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +11 -0
- package/lib/config-page.js +153 -25
- package/lib/core.js +279 -41
- package/lib/desktop-page.js +167 -40
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +34 -4
- package/lib/index.js +94 -25
- package/lib/menu.js +17 -4
- package/lib/mini-game-core.js +329 -0
- package/lib/mini-game-level-store.js +37 -0
- package/lib/mini-game-page.js +615 -0
- package/lib/mini-game.css +19 -0
- package/lib/mini-game.html +50 -0
- package/lib/scene.js +134 -7
- package/lib/vrm-info.js +16 -1
- package/package.json +3 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
|
+
<title>吃白饭 · 桌宠弹球</title>
|
|
6
|
+
<link rel="stylesheet" href="/vrmpet3d/window/lib/mini-game.css">
|
|
7
|
+
<style>.actions{flex-wrap:wrap}#test-controls{position:absolute;left:18px;bottom:64px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;max-width:90%;padding:10px;border-radius:12px;background:#101b24e8;font-size:12px}#test-controls button{padding:6px 10px}</style>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main id="game" aria-label="吃白饭,桌宠弹球游戏">
|
|
11
|
+
<div id="playfield">
|
|
12
|
+
<canvas id="arena" aria-label="鼠标移动横杆,左键发射,Esc 暂停"></canvas>
|
|
13
|
+
<canvas id="pet" aria-hidden="true"></canvas>
|
|
14
|
+
<div id="resume-countdown" hidden role="status" aria-live="polite" aria-atomic="true"><strong id="countdown-number">3</strong><span>准备继续 · Esc 返回暂停</span></div>
|
|
15
|
+
</div>
|
|
16
|
+
<header><div class="brand">吃白饭 <small>PET BREAKOUT</small></div><div></div><div class="controls"><button id="pause">暂停 <kbd>Esc</kbd></button><button id="fullscreen">全屏 ↗</button></div></header>
|
|
17
|
+
<div id="game-hud"><span id="level" aria-live="polite"></span><span id="score" aria-live="polite"></span></div>
|
|
18
|
+
<button id="notice" hidden></button>
|
|
19
|
+
<div id="lives" role="img" aria-label="备用 2 次机会"></div>
|
|
20
|
+
<p id="hint">移动鼠标控制横杆 · 左键发射 · Esc 暂停</p>
|
|
21
|
+
<p id="model-status" role="status">正在准备桌宠模型…</p>
|
|
22
|
+
<section id="overlay" hidden role="dialog" aria-modal="true" aria-labelledby="pause-title">
|
|
23
|
+
<fieldset id="debug-controls" class="model-controls" hidden><legend>DEBUG · 目前开放 2 关</legend>
|
|
24
|
+
<label for="debug-level">选择关卡</label><select id="debug-level"><option value="1">第1关 · DS / BALL</option><option value="2">第2关 · 蓝白大碗</option></select>
|
|
25
|
+
<small>切换关卡会重置所选关卡进度,保留当前机会和游戏设置。</small>
|
|
26
|
+
</fieldset>
|
|
27
|
+
<div class="panel"><span class="eyebrow">TAKE A LITTLE BREAK</span><h1 id="pause-title">休息一下</h1><p id="pause-copy">饭还热着,随时回来继续。</p><div id="sessions"></div><div class="actions"><button id="resume" class="primary">继续吃饭</button><button id="restart">重置当前关卡</button><button id="exit">保存并退出</button></div><p id="save-status" role="status"></p><p id="connection" role="status"></p></div>
|
|
28
|
+
<fieldset class="model-controls" hidden><legend>游戏模型</legend>
|
|
29
|
+
<label for="model-size">模型尺寸 <output id="model-size-value" for="model-size">90</output> px</label>
|
|
30
|
+
<input id="model-size" type="range" min="30" max="150" step="5" value="90">
|
|
31
|
+
<label for="spin-speed">旋转速度 <output id="spin-speed-value" for="spin-speed">7</output> rad/s</label>
|
|
32
|
+
<input id="spin-speed" type="range" min="0" max="30" step="1" value="7">
|
|
33
|
+
<label for="flight-speed">飞行速度 <output id="flight-speed-value" for="flight-speed">1</output> 倍</label>
|
|
34
|
+
<input id="flight-speed" type="range" min="0.5" max="2" step="0.1" value="1">
|
|
35
|
+
<label for="paddle-boost">每次撞杆加速 <output id="paddle-boost-value" for="paddle-boost">1.5</output> %</label>
|
|
36
|
+
<input id="paddle-boost" type="range" min="0" max="20" step="0.5" value="1.5">
|
|
37
|
+
<small>尺寸仅影响外观;自转设为 0 可停止旋转。飞行速度会改变游戏速度。</small>
|
|
38
|
+
</fieldset>
|
|
39
|
+
</section>
|
|
40
|
+
<aside id="test-controls" hidden aria-label="独立测试模式">
|
|
41
|
+
<strong>独立测试模式</strong><button id="test-rice">测试吃饭</button><button id="test-bowl">测试撞碗</button><button id="test-loss">模拟漏球</button><button id="test-clear">模拟通关</button><button id="test-notice">模拟消息</button>
|
|
42
|
+
</aside>
|
|
43
|
+
</main>
|
|
44
|
+
<script id="game-level-data" type="application/json">null</script>
|
|
45
|
+
<script src="/vrmpet3d/window/lib/core.js"></script>
|
|
46
|
+
<script src="/vrmpet3d/window/lib/client.vendor.js"></script>
|
|
47
|
+
<script src="/vrmpet3d/window/lib/scene.js"></script>
|
|
48
|
+
<script src="/vrmpet3d/window/lib/mini-game-core.js"></script>
|
|
49
|
+
<script src="/vrmpet3d/window/lib/mini-game-page.js"></script>
|
|
50
|
+
</body></html>
|
package/lib/scene.js
CHANGED
|
@@ -67,10 +67,20 @@
|
|
|
67
67
|
var GAZE_YAW_LIMIT = ((80 * Math.PI) / 180) / GAZE_WEIGHT;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* Automatic pitch limit, before the gaze weight. Settings expose separate actual
|
|
71
|
+
* upward/downward angles in degrees; both default to 60 degrees.
|
|
72
72
|
*/
|
|
73
|
-
var GAZE_PITCH_LIMIT =
|
|
73
|
+
var GAZE_PITCH_LIMIT = (60 * Math.PI / 180) / GAZE_WEIGHT;
|
|
74
|
+
|
|
75
|
+
function gazePitchLimits(preferences) {
|
|
76
|
+
var prefs = preferences || {};
|
|
77
|
+
function limit(key) {
|
|
78
|
+
return Number.isFinite(prefs[key])
|
|
79
|
+
? Math.max(0, Math.min(60, prefs[key])) * Math.PI / 180 / GAZE_WEIGHT
|
|
80
|
+
: GAZE_PITCH_LIMIT;
|
|
81
|
+
}
|
|
82
|
+
return { up: limit('gazePitchUp'), down: limit('gazePitchDown') };
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
76
86
|
* How far the head may turn IN TOTAL while an authored clip is driving it, in radians.
|
|
@@ -786,7 +796,8 @@
|
|
|
786
796
|
}
|
|
787
797
|
|
|
788
798
|
var yaw = Math.max(-GAZE_YAW_LIMIT, Math.min(GAZE_YAW_LIMIT, aim.yaw)) * GAZE_WEIGHT * this.gazeBlend;
|
|
789
|
-
var
|
|
799
|
+
var pitchLimits = gazePitchLimits(this.scene && this.scene.preferences);
|
|
800
|
+
var pitch = Math.max(-pitchLimits.down, Math.min(pitchLimits.up, aim.pitch)) * GAZE_WEIGHT * this.gazeBlend;
|
|
790
801
|
var axes = rest ? this.gazeAxes(head) : null;
|
|
791
802
|
|
|
792
803
|
if (layeredOnClip) {
|
|
@@ -799,8 +810,7 @@
|
|
|
799
810
|
var budget = Math.max(0, HEAD_LAYER_LIMIT - clipTurn);
|
|
800
811
|
yaw = Math.max(-budget, Math.min(budget, yaw));
|
|
801
812
|
// Pitch keeps its own (smaller) limit, but never more than what is left of the budget either.
|
|
802
|
-
pitch = Math.max(-Math.min(
|
|
803
|
-
Math.min(Math.min(GAZE_PITCH_LIMIT * GAZE_WEIGHT, budget), pitch));
|
|
813
|
+
pitch = Math.max(-budget, Math.min(budget, pitch));
|
|
804
814
|
if (axes) {
|
|
805
815
|
this._quat.setFromAxisAngle(axes.yaw, yaw);
|
|
806
816
|
this._quat2.setFromAxisAngle(axes.pitch, pitch);
|
|
@@ -1669,6 +1679,9 @@
|
|
|
1669
1679
|
if (this.disposed) return;
|
|
1670
1680
|
var w = Math.max(1, Math.round(width));
|
|
1671
1681
|
var h = Math.max(1, Math.round(height));
|
|
1682
|
+
// Layout works in fractional CSS pixels. Reapplying the same rounded size clears
|
|
1683
|
+
// the framebuffer between rAFs when a settings reply arrives during an orbit.
|
|
1684
|
+
if (this.width === w && this.height === h) return;
|
|
1672
1685
|
this.renderer.setSize(w, h, false);
|
|
1673
1686
|
this.camera.aspect = w / h;
|
|
1674
1687
|
this.camera.updateProjectionMatrix();
|
|
@@ -1755,6 +1768,8 @@
|
|
|
1755
1768
|
|
|
1756
1769
|
/** Replace the current avatar with a hardware-ready one. */
|
|
1757
1770
|
Scene.prototype.installAvatar = function (avatar, root) {
|
|
1771
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
1772
|
+
this.toonMaterials = null;
|
|
1758
1773
|
// Stop before the old avatar is replaced: stopping is what undoes the clip's
|
|
1759
1774
|
// pose, and it has to happen while the old model is still installed. Every
|
|
1760
1775
|
// clip is then dropped, because a clip is fitted to one model — the cached
|
|
@@ -1779,6 +1794,7 @@
|
|
|
1779
1794
|
this.frameAvatar(root);
|
|
1780
1795
|
this.stage.add(root);
|
|
1781
1796
|
this.avatar = avatar;
|
|
1797
|
+
this.captureToonMaterials(root);
|
|
1782
1798
|
this.kind = avatar.kind;
|
|
1783
1799
|
/**
|
|
1784
1800
|
* …and the collider switch, applied to the avatar that was just adopted.
|
|
@@ -1844,6 +1860,7 @@
|
|
|
1844
1860
|
* model is loaded, so on a fresh pet that call finds nothing to snapshot and the first sweep would have
|
|
1845
1861
|
* had no gravity to lean against (measured: `windState().joints === 0` on a real model with two chains).
|
|
1846
1862
|
*/
|
|
1863
|
+
this.applySprings(this.preferences);
|
|
1847
1864
|
this.captureSpringBase();
|
|
1848
1865
|
this.expressionBehaviour = { blink: 0, blinkExpression: 'blink', emotion: null, emotionWeight: 0 };
|
|
1849
1866
|
this.expressionState = { nextBlinkAt: null, blinkUntil: 0, nextIdleEmotionAt: null, emotionUntil: 0 };
|
|
@@ -1852,6 +1869,73 @@
|
|
|
1852
1869
|
this.motion.setGazeIdle(this.preferences.gazeIdleSeconds);
|
|
1853
1870
|
this.updateShadowPlane();
|
|
1854
1871
|
this.refitCamera();
|
|
1872
|
+
this.applyToonPreferences();
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
/** Keep the loaded materials (and expression bindings) intact so the switch is reversible. */
|
|
1876
|
+
Scene.prototype.captureToonMaterials = function (root) {
|
|
1877
|
+
var self = this;
|
|
1878
|
+
this.toonMeshes = [];
|
|
1879
|
+
this.toonMaterials = [];
|
|
1880
|
+
var seen = new Map();
|
|
1881
|
+
root.traverse(function (node) {
|
|
1882
|
+
if (!node.isMesh || !node.material) return;
|
|
1883
|
+
var originals = Array.isArray(node.material) ? node.material : [node.material];
|
|
1884
|
+
var entries = originals.map(function (material) {
|
|
1885
|
+
if (!material.isMToonMaterial) return null;
|
|
1886
|
+
if (seen.has(material)) return seen.get(material);
|
|
1887
|
+
var entry = { material: material, toony: material.shadingToonyFactor, shift: material.shadingShiftFactor,
|
|
1888
|
+
shade: material.shadeColorFactor.clone(), width: material.outlineWidthFactor, plain: null };
|
|
1889
|
+
seen.set(material, entry);
|
|
1890
|
+
self.toonMaterials.push(entry);
|
|
1891
|
+
return entry;
|
|
1892
|
+
});
|
|
1893
|
+
if (entries.some(Boolean)) self.toonMeshes.push({ node: node, original: node.material, entries: entries, originals: originals });
|
|
1894
|
+
});
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
Scene.prototype.applyToonPreferences = function () {
|
|
1898
|
+
var self = this;
|
|
1899
|
+
var id = this.currentModelId || this.preferences.modelId;
|
|
1900
|
+
var prefs = (this.preferences.toon || {})[id] || {};
|
|
1901
|
+
var disabled = prefs.enabled === false;
|
|
1902
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1903
|
+
var material = entry.material;
|
|
1904
|
+
material.shadingToonyFactor = prefs.shadingToonyFactor === undefined ? entry.toony : prefs.shadingToonyFactor;
|
|
1905
|
+
material.shadingShiftFactor = prefs.shadingShiftFactor === undefined ? entry.shift : prefs.shadingShiftFactor;
|
|
1906
|
+
material.shadeColorFactor.copy(entry.shade);
|
|
1907
|
+
if (prefs.shadeStrength !== undefined) material.shadeColorFactor.lerp(material.color, 1 - prefs.shadeStrength);
|
|
1908
|
+
material.outlineWidthFactor = entry.width * (prefs.outlineScale === undefined ? 1 : prefs.outlineScale);
|
|
1909
|
+
if (disabled && !entry.plain) {
|
|
1910
|
+
var plain = new self.THREE.MeshStandardMaterial();
|
|
1911
|
+
['map', 'alphaMap', 'alphaTest', 'transparent', 'opacity', 'side', 'depthWrite', 'depthTest',
|
|
1912
|
+
'normalMap', 'normalScale', 'emissive', 'emissiveMap', 'emissiveIntensity', 'color'].forEach(function (key) {
|
|
1913
|
+
if (material[key] === undefined) return;
|
|
1914
|
+
if (plain[key] && typeof plain[key].copy === 'function') plain[key].copy(material[key]);
|
|
1915
|
+
else plain[key] = material[key];
|
|
1916
|
+
});
|
|
1917
|
+
plain.visible = !material.isOutline;
|
|
1918
|
+
entry.plain = plain;
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
(this.toonMeshes || []).forEach(function (mesh) {
|
|
1922
|
+
mesh.node.material = disabled
|
|
1923
|
+
? (Array.isArray(mesh.original) ? mesh.originals.map(function (m, i) { return mesh.entries[i] ? mesh.entries[i].plain : m; })
|
|
1924
|
+
: mesh.entries[0].plain)
|
|
1925
|
+
: mesh.original;
|
|
1926
|
+
});
|
|
1927
|
+
this.toonDisabled = disabled;
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
Scene.prototype.syncPlainMaterials = function () {
|
|
1931
|
+
if (!this.toonDisabled) return;
|
|
1932
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1933
|
+
if (!entry.plain) return;
|
|
1934
|
+
// Expressions still target the authored MToon material, including UV animation.
|
|
1935
|
+
entry.plain.color.copy(entry.material.color);
|
|
1936
|
+
entry.plain.emissive.copy(entry.material.emissive);
|
|
1937
|
+
entry.plain.opacity = entry.material.opacity;
|
|
1938
|
+
});
|
|
1855
1939
|
};
|
|
1856
1940
|
|
|
1857
1941
|
/** A soft ground blob, so the pet does not appear to float. */
|
|
@@ -3380,7 +3464,8 @@
|
|
|
3380
3464
|
var x = Math.max(-1, Math.min(1, aim.x));
|
|
3381
3465
|
var y = Math.max(-1, Math.min(1, aim.y));
|
|
3382
3466
|
// Screen y grows downward, pitch grows upward.
|
|
3383
|
-
|
|
3467
|
+
var limits = gazePitchLimits(this.preferences);
|
|
3468
|
+
return { yaw: x * GAZE_YAW_LIMIT, pitch: -y * (y < 0 ? limits.up : limits.down) };
|
|
3384
3469
|
};
|
|
3385
3470
|
|
|
3386
3471
|
/** Advance one frame. `dt` is in seconds. */
|
|
@@ -3437,6 +3522,7 @@
|
|
|
3437
3522
|
this.advanceExpressions(dt);
|
|
3438
3523
|
this.applyExpressions(this.preferences);
|
|
3439
3524
|
|
|
3525
|
+
this.syncPlainMaterials();
|
|
3440
3526
|
this.renderer.render(this.scene, this.camera);
|
|
3441
3527
|
this.frame += 1;
|
|
3442
3528
|
};
|
|
@@ -3448,6 +3534,7 @@
|
|
|
3448
3534
|
Scene.prototype.applyPreferences = function (preferences) {
|
|
3449
3535
|
var raw = preferences && typeof preferences === 'object' ? preferences : {};
|
|
3450
3536
|
this.preferences = this.core.cleanPreferences(preferences);
|
|
3537
|
+
this.applyToonPreferences();
|
|
3451
3538
|
// A preferences push only carries `motions` when the caller meant to change
|
|
3452
3539
|
// it. cleanPreferences always produces a (possibly empty) key, so adopting it
|
|
3453
3540
|
// unconditionally would silently wipe a map set through setMotionMap.
|
|
@@ -3641,6 +3728,40 @@
|
|
|
3641
3728
|
};
|
|
3642
3729
|
};
|
|
3643
3730
|
|
|
3731
|
+
/** The rendered eyes' midpoint, in canvas CSS pixels, independent of framing and camera orbit. */
|
|
3732
|
+
Scene.prototype.gazeOrigin = function () {
|
|
3733
|
+
var avatar = this.avatar;
|
|
3734
|
+
if (!avatar || !this.THREE || !this.camera) return null;
|
|
3735
|
+
var point = this._gazeOriginVec || (this._gazeOriginVec = new this.THREE.Vector3());
|
|
3736
|
+
var other = this._gazeOtherEyeVec || (this._gazeOtherEyeVec = new this.THREE.Vector3());
|
|
3737
|
+
var humanoid = avatar.vrm && avatar.vrm.humanoid;
|
|
3738
|
+
var left = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('leftEye') : null;
|
|
3739
|
+
var right = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('rightEye') : null;
|
|
3740
|
+
left = left || (avatar.bones && avatar.bones.leftEye);
|
|
3741
|
+
right = right || (avatar.bones && avatar.bones.rightEye);
|
|
3742
|
+
var source;
|
|
3743
|
+
if (left && right) {
|
|
3744
|
+
left.getWorldPosition(point); right.getWorldPosition(other);
|
|
3745
|
+
point.add(other).multiplyScalar(0.5);
|
|
3746
|
+
source = 'eyes';
|
|
3747
|
+
} else if (avatar.vrm && avatar.vrm.lookAt && typeof avatar.vrm.lookAt.getLookAtWorldPosition === 'function') {
|
|
3748
|
+
// The loader has already converted VRM 0/1 coordinate conventions for this offset.
|
|
3749
|
+
if (avatar.root) avatar.root.updateWorldMatrix(true, true);
|
|
3750
|
+
avatar.vrm.lookAt.getLookAtWorldPosition(point);
|
|
3751
|
+
source = 'lookAt';
|
|
3752
|
+
} else {
|
|
3753
|
+
var head = gazeNodeFor(avatar);
|
|
3754
|
+
if (!head) return null;
|
|
3755
|
+
head.getWorldPosition(point);
|
|
3756
|
+
source = 'head';
|
|
3757
|
+
}
|
|
3758
|
+
this.camera.updateMatrixWorld(true);
|
|
3759
|
+
var projected = this.projectToCanvas(point);
|
|
3760
|
+
if (!projected || projected.z < -1 || projected.z > 1) return null;
|
|
3761
|
+
projected.source = source;
|
|
3762
|
+
return projected;
|
|
3763
|
+
};
|
|
3764
|
+
|
|
3644
3765
|
/**
|
|
3645
3766
|
* Apply the draught to the chains, decay it, and keep it LOCAL.
|
|
3646
3767
|
*
|
|
@@ -3865,6 +3986,10 @@
|
|
|
3865
3986
|
*/
|
|
3866
3987
|
Scene.prototype.setSpringDefaults = function (defaults) {
|
|
3867
3988
|
this.springDefaults = defaults && typeof defaults === 'object' ? defaults : null;
|
|
3989
|
+
// Catalog defaults can arrive after the avatar and preferences on either surface.
|
|
3990
|
+
// Apply them now; waiting for another preference edit makes a reload appear to fix physics.
|
|
3991
|
+
this.applySprings(this.preferences);
|
|
3992
|
+
this.captureSpringBase();
|
|
3868
3993
|
};
|
|
3869
3994
|
|
|
3870
3995
|
/**
|
|
@@ -4931,6 +5056,8 @@
|
|
|
4931
5056
|
|
|
4932
5057
|
Scene.prototype.dispose = function () {
|
|
4933
5058
|
if (this.disposed) return;
|
|
5059
|
+
(this.toonMeshes || []).forEach(function (mesh) { mesh.node.material = mesh.original; });
|
|
5060
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
4934
5061
|
this.disposed = true;
|
|
4935
5062
|
// Stop and uncache every mixer before the renderer goes: a mixer holds a
|
|
4936
5063
|
// binding table that references the model's bones, so leaving them behind
|
package/lib/vrm-info.js
CHANGED
|
@@ -437,7 +437,7 @@ export function describeGlb(glb, label) {
|
|
|
437
437
|
(gltf.materials || []).forEach((material, index) => {
|
|
438
438
|
const pbr = material.pbrMetallicRoughness || {};
|
|
439
439
|
const extensions = material.extensions || {};
|
|
440
|
-
const mtoon =
|
|
440
|
+
const mtoon = vrm0 && (vrm0.materialProperties || [])[index];
|
|
441
441
|
const mtoon1 = extensions.VRMC_materials_mtoon || null;
|
|
442
442
|
report.materials.push({
|
|
443
443
|
index,
|
|
@@ -826,6 +826,21 @@ export function cacheStats() {
|
|
|
826
826
|
*/
|
|
827
827
|
export function compactModelInfo(info) {
|
|
828
828
|
return {
|
|
829
|
+
toon: {
|
|
830
|
+
total: (info.materials || []).length,
|
|
831
|
+
materials: (info.materials || []).filter((m) => m.mtoon1 || (m.mtoon && m.mtoon.shader === 'VRM/MToon')).map((m) => {
|
|
832
|
+
// Same VRM 0 -> MToon 1 conversion as the pinned three-vrm loader.
|
|
833
|
+
const legacy = m.mtoon && m.mtoon.floatProperties || {};
|
|
834
|
+
const shift = legacy._ShadeShift ?? 0;
|
|
835
|
+
const toony = (legacy._ShadeToony ?? 0.9) + (1 - (legacy._ShadeToony ?? 0.9)) * (0.5 + 0.5 * shift);
|
|
836
|
+
return {
|
|
837
|
+
name: m.name,
|
|
838
|
+
shadingToonyFactor: m.mtoon1 ? (m.mtoon1.shadingToonyFactor ?? 0.9) : toony,
|
|
839
|
+
shadingShiftFactor: m.mtoon1 ? (m.mtoon1.shadingShiftFactor ?? 0) : -shift - (1 - toony),
|
|
840
|
+
outline: m.mtoon1 ? Boolean(m.mtoon1.outlineWidthMode && m.mtoon1.outlineWidthMode !== 'none' && m.mtoon1.outlineWidthFactor > 0)
|
|
841
|
+
: Boolean(m.mtoon.floatProperties._OutlineWidthMode && m.mtoon.floatProperties._OutlineWidth > 0),
|
|
842
|
+
}; }),
|
|
843
|
+
},
|
|
829
844
|
supported: info.supported === true,
|
|
830
845
|
vrmSpec: info.vrmSpec,
|
|
831
846
|
generator: info.generator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-vrmpet3d",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DeepSeek Harness 的 VRM 3D 桌宠:任务状态联动、独立悬浮窗、模型动作与 AI 碎碎念。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
"displayName": "桌宠 3D · VRM Pet 3D",
|
|
40
39
|
"repository": {
|
|
41
40
|
"type": "git",
|
|
42
41
|
"url": "https://github.com/drliuhuan/dsh-plugin-vrmpet3d.git"
|
|
@@ -53,5 +52,6 @@
|
|
|
53
52
|
"vrma",
|
|
54
53
|
"threejs",
|
|
55
54
|
"electron"
|
|
56
|
-
]
|
|
55
|
+
],
|
|
56
|
+
"displayName": "桌宠 3D · VRM Pet 3D"
|
|
57
57
|
}
|