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
package/lib/client.scene.js
CHANGED
|
@@ -75,10 +75,20 @@
|
|
|
75
75
|
var GAZE_YAW_LIMIT = ((80 * Math.PI) / 180) / GAZE_WEIGHT;
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* Automatic pitch limit, before the gaze weight. Settings expose separate actual
|
|
79
|
+
* upward/downward angles in degrees; both default to 60 degrees.
|
|
80
80
|
*/
|
|
81
|
-
var GAZE_PITCH_LIMIT =
|
|
81
|
+
var GAZE_PITCH_LIMIT = (60 * Math.PI / 180) / GAZE_WEIGHT;
|
|
82
|
+
|
|
83
|
+
function gazePitchLimits(preferences) {
|
|
84
|
+
var prefs = preferences || {};
|
|
85
|
+
function limit(key) {
|
|
86
|
+
return Number.isFinite(prefs[key])
|
|
87
|
+
? Math.max(0, Math.min(60, prefs[key])) * Math.PI / 180 / GAZE_WEIGHT
|
|
88
|
+
: GAZE_PITCH_LIMIT;
|
|
89
|
+
}
|
|
90
|
+
return { up: limit('gazePitchUp'), down: limit('gazePitchDown') };
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
/**
|
|
84
94
|
* How far the head may turn IN TOTAL while an authored clip is driving it, in radians.
|
|
@@ -794,7 +804,8 @@
|
|
|
794
804
|
}
|
|
795
805
|
|
|
796
806
|
var yaw = Math.max(-GAZE_YAW_LIMIT, Math.min(GAZE_YAW_LIMIT, aim.yaw)) * GAZE_WEIGHT * this.gazeBlend;
|
|
797
|
-
var
|
|
807
|
+
var pitchLimits = gazePitchLimits(this.scene && this.scene.preferences);
|
|
808
|
+
var pitch = Math.max(-pitchLimits.down, Math.min(pitchLimits.up, aim.pitch)) * GAZE_WEIGHT * this.gazeBlend;
|
|
798
809
|
var axes = rest ? this.gazeAxes(head) : null;
|
|
799
810
|
|
|
800
811
|
if (layeredOnClip) {
|
|
@@ -807,8 +818,7 @@
|
|
|
807
818
|
var budget = Math.max(0, HEAD_LAYER_LIMIT - clipTurn);
|
|
808
819
|
yaw = Math.max(-budget, Math.min(budget, yaw));
|
|
809
820
|
// Pitch keeps its own (smaller) limit, but never more than what is left of the budget either.
|
|
810
|
-
pitch = Math.max(-Math.min(
|
|
811
|
-
Math.min(Math.min(GAZE_PITCH_LIMIT * GAZE_WEIGHT, budget), pitch));
|
|
821
|
+
pitch = Math.max(-budget, Math.min(budget, pitch));
|
|
812
822
|
if (axes) {
|
|
813
823
|
this._quat.setFromAxisAngle(axes.yaw, yaw);
|
|
814
824
|
this._quat2.setFromAxisAngle(axes.pitch, pitch);
|
|
@@ -1677,6 +1687,9 @@
|
|
|
1677
1687
|
if (this.disposed) return;
|
|
1678
1688
|
var w = Math.max(1, Math.round(width));
|
|
1679
1689
|
var h = Math.max(1, Math.round(height));
|
|
1690
|
+
// Layout works in fractional CSS pixels. Reapplying the same rounded size clears
|
|
1691
|
+
// the framebuffer between rAFs when a settings reply arrives during an orbit.
|
|
1692
|
+
if (this.width === w && this.height === h) return;
|
|
1680
1693
|
this.renderer.setSize(w, h, false);
|
|
1681
1694
|
this.camera.aspect = w / h;
|
|
1682
1695
|
this.camera.updateProjectionMatrix();
|
|
@@ -1763,6 +1776,8 @@
|
|
|
1763
1776
|
|
|
1764
1777
|
/** Replace the current avatar with a hardware-ready one. */
|
|
1765
1778
|
Scene.prototype.installAvatar = function (avatar, root) {
|
|
1779
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
1780
|
+
this.toonMaterials = null;
|
|
1766
1781
|
// Stop before the old avatar is replaced: stopping is what undoes the clip's
|
|
1767
1782
|
// pose, and it has to happen while the old model is still installed. Every
|
|
1768
1783
|
// clip is then dropped, because a clip is fitted to one model — the cached
|
|
@@ -1787,6 +1802,7 @@
|
|
|
1787
1802
|
this.frameAvatar(root);
|
|
1788
1803
|
this.stage.add(root);
|
|
1789
1804
|
this.avatar = avatar;
|
|
1805
|
+
this.captureToonMaterials(root);
|
|
1790
1806
|
this.kind = avatar.kind;
|
|
1791
1807
|
/**
|
|
1792
1808
|
* …and the collider switch, applied to the avatar that was just adopted.
|
|
@@ -1852,6 +1868,7 @@
|
|
|
1852
1868
|
* model is loaded, so on a fresh pet that call finds nothing to snapshot and the first sweep would have
|
|
1853
1869
|
* had no gravity to lean against (measured: `windState().joints === 0` on a real model with two chains).
|
|
1854
1870
|
*/
|
|
1871
|
+
this.applySprings(this.preferences);
|
|
1855
1872
|
this.captureSpringBase();
|
|
1856
1873
|
this.expressionBehaviour = { blink: 0, blinkExpression: 'blink', emotion: null, emotionWeight: 0 };
|
|
1857
1874
|
this.expressionState = { nextBlinkAt: null, blinkUntil: 0, nextIdleEmotionAt: null, emotionUntil: 0 };
|
|
@@ -1860,6 +1877,73 @@
|
|
|
1860
1877
|
this.motion.setGazeIdle(this.preferences.gazeIdleSeconds);
|
|
1861
1878
|
this.updateShadowPlane();
|
|
1862
1879
|
this.refitCamera();
|
|
1880
|
+
this.applyToonPreferences();
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
/** Keep the loaded materials (and expression bindings) intact so the switch is reversible. */
|
|
1884
|
+
Scene.prototype.captureToonMaterials = function (root) {
|
|
1885
|
+
var self = this;
|
|
1886
|
+
this.toonMeshes = [];
|
|
1887
|
+
this.toonMaterials = [];
|
|
1888
|
+
var seen = new Map();
|
|
1889
|
+
root.traverse(function (node) {
|
|
1890
|
+
if (!node.isMesh || !node.material) return;
|
|
1891
|
+
var originals = Array.isArray(node.material) ? node.material : [node.material];
|
|
1892
|
+
var entries = originals.map(function (material) {
|
|
1893
|
+
if (!material.isMToonMaterial) return null;
|
|
1894
|
+
if (seen.has(material)) return seen.get(material);
|
|
1895
|
+
var entry = { material: material, toony: material.shadingToonyFactor, shift: material.shadingShiftFactor,
|
|
1896
|
+
shade: material.shadeColorFactor.clone(), width: material.outlineWidthFactor, plain: null };
|
|
1897
|
+
seen.set(material, entry);
|
|
1898
|
+
self.toonMaterials.push(entry);
|
|
1899
|
+
return entry;
|
|
1900
|
+
});
|
|
1901
|
+
if (entries.some(Boolean)) self.toonMeshes.push({ node: node, original: node.material, entries: entries, originals: originals });
|
|
1902
|
+
});
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
Scene.prototype.applyToonPreferences = function () {
|
|
1906
|
+
var self = this;
|
|
1907
|
+
var id = this.currentModelId || this.preferences.modelId;
|
|
1908
|
+
var prefs = (this.preferences.toon || {})[id] || {};
|
|
1909
|
+
var disabled = prefs.enabled === false;
|
|
1910
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1911
|
+
var material = entry.material;
|
|
1912
|
+
material.shadingToonyFactor = prefs.shadingToonyFactor === undefined ? entry.toony : prefs.shadingToonyFactor;
|
|
1913
|
+
material.shadingShiftFactor = prefs.shadingShiftFactor === undefined ? entry.shift : prefs.shadingShiftFactor;
|
|
1914
|
+
material.shadeColorFactor.copy(entry.shade);
|
|
1915
|
+
if (prefs.shadeStrength !== undefined) material.shadeColorFactor.lerp(material.color, 1 - prefs.shadeStrength);
|
|
1916
|
+
material.outlineWidthFactor = entry.width * (prefs.outlineScale === undefined ? 1 : prefs.outlineScale);
|
|
1917
|
+
if (disabled && !entry.plain) {
|
|
1918
|
+
var plain = new self.THREE.MeshStandardMaterial();
|
|
1919
|
+
['map', 'alphaMap', 'alphaTest', 'transparent', 'opacity', 'side', 'depthWrite', 'depthTest',
|
|
1920
|
+
'normalMap', 'normalScale', 'emissive', 'emissiveMap', 'emissiveIntensity', 'color'].forEach(function (key) {
|
|
1921
|
+
if (material[key] === undefined) return;
|
|
1922
|
+
if (plain[key] && typeof plain[key].copy === 'function') plain[key].copy(material[key]);
|
|
1923
|
+
else plain[key] = material[key];
|
|
1924
|
+
});
|
|
1925
|
+
plain.visible = !material.isOutline;
|
|
1926
|
+
entry.plain = plain;
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
(this.toonMeshes || []).forEach(function (mesh) {
|
|
1930
|
+
mesh.node.material = disabled
|
|
1931
|
+
? (Array.isArray(mesh.original) ? mesh.originals.map(function (m, i) { return mesh.entries[i] ? mesh.entries[i].plain : m; })
|
|
1932
|
+
: mesh.entries[0].plain)
|
|
1933
|
+
: mesh.original;
|
|
1934
|
+
});
|
|
1935
|
+
this.toonDisabled = disabled;
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
Scene.prototype.syncPlainMaterials = function () {
|
|
1939
|
+
if (!this.toonDisabled) return;
|
|
1940
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1941
|
+
if (!entry.plain) return;
|
|
1942
|
+
// Expressions still target the authored MToon material, including UV animation.
|
|
1943
|
+
entry.plain.color.copy(entry.material.color);
|
|
1944
|
+
entry.plain.emissive.copy(entry.material.emissive);
|
|
1945
|
+
entry.plain.opacity = entry.material.opacity;
|
|
1946
|
+
});
|
|
1863
1947
|
};
|
|
1864
1948
|
|
|
1865
1949
|
/** A soft ground blob, so the pet does not appear to float. */
|
|
@@ -3388,7 +3472,8 @@
|
|
|
3388
3472
|
var x = Math.max(-1, Math.min(1, aim.x));
|
|
3389
3473
|
var y = Math.max(-1, Math.min(1, aim.y));
|
|
3390
3474
|
// Screen y grows downward, pitch grows upward.
|
|
3391
|
-
|
|
3475
|
+
var limits = gazePitchLimits(this.preferences);
|
|
3476
|
+
return { yaw: x * GAZE_YAW_LIMIT, pitch: -y * (y < 0 ? limits.up : limits.down) };
|
|
3392
3477
|
};
|
|
3393
3478
|
|
|
3394
3479
|
/** Advance one frame. `dt` is in seconds. */
|
|
@@ -3445,6 +3530,7 @@
|
|
|
3445
3530
|
this.advanceExpressions(dt);
|
|
3446
3531
|
this.applyExpressions(this.preferences);
|
|
3447
3532
|
|
|
3533
|
+
this.syncPlainMaterials();
|
|
3448
3534
|
this.renderer.render(this.scene, this.camera);
|
|
3449
3535
|
this.frame += 1;
|
|
3450
3536
|
};
|
|
@@ -3456,6 +3542,7 @@
|
|
|
3456
3542
|
Scene.prototype.applyPreferences = function (preferences) {
|
|
3457
3543
|
var raw = preferences && typeof preferences === 'object' ? preferences : {};
|
|
3458
3544
|
this.preferences = this.core.cleanPreferences(preferences);
|
|
3545
|
+
this.applyToonPreferences();
|
|
3459
3546
|
// A preferences push only carries `motions` when the caller meant to change
|
|
3460
3547
|
// it. cleanPreferences always produces a (possibly empty) key, so adopting it
|
|
3461
3548
|
// unconditionally would silently wipe a map set through setMotionMap.
|
|
@@ -3649,6 +3736,40 @@
|
|
|
3649
3736
|
};
|
|
3650
3737
|
};
|
|
3651
3738
|
|
|
3739
|
+
/** The rendered eyes' midpoint, in canvas CSS pixels, independent of framing and camera orbit. */
|
|
3740
|
+
Scene.prototype.gazeOrigin = function () {
|
|
3741
|
+
var avatar = this.avatar;
|
|
3742
|
+
if (!avatar || !this.THREE || !this.camera) return null;
|
|
3743
|
+
var point = this._gazeOriginVec || (this._gazeOriginVec = new this.THREE.Vector3());
|
|
3744
|
+
var other = this._gazeOtherEyeVec || (this._gazeOtherEyeVec = new this.THREE.Vector3());
|
|
3745
|
+
var humanoid = avatar.vrm && avatar.vrm.humanoid;
|
|
3746
|
+
var left = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('leftEye') : null;
|
|
3747
|
+
var right = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('rightEye') : null;
|
|
3748
|
+
left = left || (avatar.bones && avatar.bones.leftEye);
|
|
3749
|
+
right = right || (avatar.bones && avatar.bones.rightEye);
|
|
3750
|
+
var source;
|
|
3751
|
+
if (left && right) {
|
|
3752
|
+
left.getWorldPosition(point); right.getWorldPosition(other);
|
|
3753
|
+
point.add(other).multiplyScalar(0.5);
|
|
3754
|
+
source = 'eyes';
|
|
3755
|
+
} else if (avatar.vrm && avatar.vrm.lookAt && typeof avatar.vrm.lookAt.getLookAtWorldPosition === 'function') {
|
|
3756
|
+
// The loader has already converted VRM 0/1 coordinate conventions for this offset.
|
|
3757
|
+
if (avatar.root) avatar.root.updateWorldMatrix(true, true);
|
|
3758
|
+
avatar.vrm.lookAt.getLookAtWorldPosition(point);
|
|
3759
|
+
source = 'lookAt';
|
|
3760
|
+
} else {
|
|
3761
|
+
var head = gazeNodeFor(avatar);
|
|
3762
|
+
if (!head) return null;
|
|
3763
|
+
head.getWorldPosition(point);
|
|
3764
|
+
source = 'head';
|
|
3765
|
+
}
|
|
3766
|
+
this.camera.updateMatrixWorld(true);
|
|
3767
|
+
var projected = this.projectToCanvas(point);
|
|
3768
|
+
if (!projected || projected.z < -1 || projected.z > 1) return null;
|
|
3769
|
+
projected.source = source;
|
|
3770
|
+
return projected;
|
|
3771
|
+
};
|
|
3772
|
+
|
|
3652
3773
|
/**
|
|
3653
3774
|
* Apply the draught to the chains, decay it, and keep it LOCAL.
|
|
3654
3775
|
*
|
|
@@ -3873,6 +3994,10 @@
|
|
|
3873
3994
|
*/
|
|
3874
3995
|
Scene.prototype.setSpringDefaults = function (defaults) {
|
|
3875
3996
|
this.springDefaults = defaults && typeof defaults === 'object' ? defaults : null;
|
|
3997
|
+
// Catalog defaults can arrive after the avatar and preferences on either surface.
|
|
3998
|
+
// Apply them now; waiting for another preference edit makes a reload appear to fix physics.
|
|
3999
|
+
this.applySprings(this.preferences);
|
|
4000
|
+
this.captureSpringBase();
|
|
3876
4001
|
};
|
|
3877
4002
|
|
|
3878
4003
|
/**
|
|
@@ -4939,6 +5064,8 @@
|
|
|
4939
5064
|
|
|
4940
5065
|
Scene.prototype.dispose = function () {
|
|
4941
5066
|
if (this.disposed) return;
|
|
5067
|
+
(this.toonMeshes || []).forEach(function (mesh) { mesh.node.material = mesh.original; });
|
|
5068
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
4942
5069
|
this.disposed = true;
|
|
4943
5070
|
// Stop and uncache every mixer before the renderer goes: a mixer holds a
|
|
4944
5071
|
// binding table that references the model's bones, so leaving them behind
|
package/lib/config-page.html
CHANGED
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
<div class="row"><span class="k" data-i18n="availableInfo">可用信息</span><span class="grow note" id="model-meta">—</span></div>
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
|
+
<div class="card" id="toon-card" style="margin-top:12px">
|
|
57
|
+
<h3 data-i18n="toonTitle">卡通增强</h3>
|
|
58
|
+
<div class="row"><span class="k" data-i18n="toonState">材质状态</span><span class="grow note" id="toon-state" role="status">—</span></div>
|
|
59
|
+
<div class="row">
|
|
60
|
+
<label class="k" for="toon-mode">MToon</label>
|
|
61
|
+
<select id="toon-mode"><option value="original" data-i18n="toonOriginal">模型原始</option><option value="on" data-i18n="on">开启</option><option value="off" data-i18n="toonDisabled">关闭</option></select>
|
|
62
|
+
<button id="toon-reset" type="button" data-i18n="toonReset">恢复模型原始参数</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="toon-controls"></div>
|
|
65
|
+
<p class="note" data-i18n="toonNote">参数仅保存到当前模型,留空使用模型原始值。关闭 MToon 后使用普通光照材质。描边倍率只调整模型已有的描边。</p>
|
|
66
|
+
</div>
|
|
56
67
|
</section>
|
|
57
68
|
|
|
58
69
|
<section class="settings-section" id="settings-1">
|
package/lib/config-page.js
CHANGED
|
@@ -91,6 +91,20 @@
|
|
|
91
91
|
zh: {
|
|
92
92
|
// ---- the chrome, and the name each page gives itself ----
|
|
93
93
|
titleModel: 'vrmpet3d · 模型参数',
|
|
94
|
+
gazePitchUp: '最大抬头角度', gazePitchDown: '最大低头角度', gazePitchTest: '测试 20° / 30°',
|
|
95
|
+
gazeCurveMode: '曲线模式', gazeAxes: '分轴曲线', gazeRadial: '二维距离曲线',
|
|
96
|
+
gazeSpatial: '空间角度', gazeCurveSpatial: '空间角度曲率', gazeDistance: '注视距离',
|
|
97
|
+
gazeSpatialHint: '距离以满偏距离为单位,越大转头越缓。同高时,鼠标越偏向左右,俯仰越小;与双眼同高时俯仰为零。默认曲率 1、注视距离 0.1。',
|
|
98
|
+
gazeSpanHint: '满偏距离填 0 为自动:水平和垂直均使用屏幕宽度的一半。',
|
|
99
|
+
gazeCurveX: '水平曲率', gazeCurveY: '垂直曲率', gazeCurveRadial: '二维距离曲率',
|
|
100
|
+
gazeCurveHint: '数值越小,中心附近越灵敏;1 为线性。二维距离模式按满偏距离归一化后计算方向与强度,可减少远处光标上下穿过视线水平线时的俯仰跳变。各模式的参数分别保存。',
|
|
101
|
+
gazePitchHint: '自动为抬头、低头各 60°。范围 0–60°;0 表示不向该方向转头。这是视线跟踪的附加角度,动作本身的头部姿势仍会保留。',
|
|
102
|
+
toonTitle: '卡通增强', toonState: '材质状态', toonOriginal: '模型原始', toonReset: '恢复模型原始参数',
|
|
103
|
+
toonNote: '参数仅保存到当前模型,留空使用模型原始值。关闭 MToon 后使用普通光照材质。描边倍率只调整模型已有的描边。',
|
|
104
|
+
toonLoading: '正在读取当前模型材质…', toonUnsupported: '当前模型未声明 MToon 材质,暂不支持转换。',
|
|
105
|
+
toonStatus: '{n}/{total} 个原生 MToon 材质 · 当前{state}', toonEnabled: '开启', toonDisabled: '关闭',
|
|
106
|
+
toonToony: '明暗分界硬度', toonShift: '明暗分界偏移', toonShade: '阴影强度', toonOutline: '描边倍率',
|
|
107
|
+
toonMixed: '模型原始(各材质不同)', toonNoOutline: '模型没有启用描边',
|
|
94
108
|
titleGlobal: 'vrmpet3d · 通用设置',
|
|
95
109
|
headingModel: '模型参数',
|
|
96
110
|
headingGlobal: '通用设置',
|
|
@@ -417,6 +431,20 @@
|
|
|
417
431
|
en: {
|
|
418
432
|
// ---- the chrome, and the name each page gives itself ----
|
|
419
433
|
titleModel: 'vrmpet3d · Model parameters',
|
|
434
|
+
gazePitchUp: 'Maximum upward pitch', gazePitchDown: 'Maximum downward pitch', gazePitchTest: 'Try 20° / 30°',
|
|
435
|
+
gazeCurveMode: 'Curve mode', gazeAxes: 'Separate axes', gazeRadial: '2D distance',
|
|
436
|
+
gazeSpatial: 'Spatial angles', gazeCurveSpatial: 'Spatial angle curve', gazeDistance: 'Target distance',
|
|
437
|
+
gazeSpatialHint: 'Distance is in span units; larger values soften head turns. At a fixed height, moving sideways reduces pitch. At eye level, pitch is zero. Defaults: curve 1, target distance 0.1.',
|
|
438
|
+
gazeSpanHint: 'Enter 0 for automatic spans: both axes use half the screen width.',
|
|
439
|
+
gazeCurveX: 'Horizontal curve', gazeCurveY: 'Vertical curve', gazeCurveRadial: '2D distance curve',
|
|
440
|
+
gazeCurveHint: 'Lower values increase sensitivity near the center; 1 is linear. The 2D mode uses distance normalized by each axis span, reducing abrupt pitch changes as a distant pointer crosses eye level. Each mode keeps its own values.',
|
|
441
|
+
gazePitchHint: 'Automatic: 60° up and down. Range: 0–60°. Zero disables tracking in that direction. Gaze adds these angles while preserving the animation’s head pose.',
|
|
442
|
+
toonTitle: 'Cartoon enhancement', toonState: 'Material status', toonOriginal: 'Model original', toonReset: 'Restore original materials',
|
|
443
|
+
toonNote: 'Saved for this model only. Leave a value blank to use its original. Turning MToon off uses standard lit materials. Outline scale adjusts existing outlines only.',
|
|
444
|
+
toonLoading: 'Reading model materials…', toonUnsupported: 'This model has no MToon materials. Conversion is not supported yet.',
|
|
445
|
+
toonStatus: '{n}/{total} native MToon materials · Currently {state}', toonEnabled: 'on', toonDisabled: 'off',
|
|
446
|
+
toonToony: 'Shading hardness', toonShift: 'Shading shift', toonShade: 'Shade strength', toonOutline: 'Outline scale',
|
|
447
|
+
toonMixed: 'Original (varies by material)', toonNoOutline: 'This model has no enabled outlines',
|
|
420
448
|
titleGlobal: 'vrmpet3d · General settings',
|
|
421
449
|
headingModel: 'Model parameters',
|
|
422
450
|
headingGlobal: 'General settings',
|
|
@@ -1523,6 +1551,7 @@
|
|
|
1523
1551
|
}
|
|
1524
1552
|
|
|
1525
1553
|
function renderModel() {
|
|
1554
|
+
renderToon();
|
|
1526
1555
|
var spec = element('model-spec');
|
|
1527
1556
|
var meta = element('model-meta');
|
|
1528
1557
|
if (!spec || !meta) return;
|
|
@@ -1553,6 +1582,67 @@
|
|
|
1553
1582
|
// ---------------------------------------------------------------------------
|
|
1554
1583
|
// Expressions
|
|
1555
1584
|
// ---------------------------------------------------------------------------
|
|
1585
|
+
function renderToon() {
|
|
1586
|
+
var card = element('toon-card');
|
|
1587
|
+
if (!card) return;
|
|
1588
|
+
var id = modelIdInUse();
|
|
1589
|
+
var info = modelInfo && modelInfoId === id && modelInfo.toon;
|
|
1590
|
+
var materials = info && info.materials || [];
|
|
1591
|
+
var supported = Boolean(id && materials.length);
|
|
1592
|
+
var prefs = (preferences.toon || {})[id] || {};
|
|
1593
|
+
var mode = element('toon-mode');
|
|
1594
|
+
var state = element('toon-state');
|
|
1595
|
+
state.textContent = !info ? (id ? t('toonLoading') : t('noModelChosen')) : !supported ? t('toonUnsupported')
|
|
1596
|
+
: t('toonStatus').replace('{n}', materials.length).replace('{total}', info.total)
|
|
1597
|
+
.replace('{state}', t(prefs.enabled === false ? 'toonDisabled' : 'toonEnabled'));
|
|
1598
|
+
mode.value = prefs.enabled === undefined ? 'original' : prefs.enabled ? 'on' : 'off';
|
|
1599
|
+
mode.disabled = !supported;
|
|
1600
|
+
function save(key, value) {
|
|
1601
|
+
if (id !== modelIdInUse()) return;
|
|
1602
|
+
var all = Object.assign({}, preferences.toon || {});
|
|
1603
|
+
var next = Object.assign({}, all[id] || {});
|
|
1604
|
+
if (value === undefined) delete next[key]; else next[key] = value;
|
|
1605
|
+
if (Object.keys(next).length) all[id] = next; else delete all[id];
|
|
1606
|
+
preferences.toon = core.cleanPreferences({ toon: all }).toon;
|
|
1607
|
+
persist();
|
|
1608
|
+
renderToon();
|
|
1609
|
+
}
|
|
1610
|
+
mode.onchange = function () { save('enabled', mode.value === 'original' ? undefined : mode.value === 'on'); };
|
|
1611
|
+
var reset = element('toon-reset');
|
|
1612
|
+
reset.disabled = !id || !Object.keys(prefs).length;
|
|
1613
|
+
reset.onclick = function () {
|
|
1614
|
+
var all = Object.assign({}, preferences.toon || {});
|
|
1615
|
+
delete all[id]; preferences.toon = all; persist(); renderToon();
|
|
1616
|
+
};
|
|
1617
|
+
var controls = element('toon-controls');
|
|
1618
|
+
controls.textContent = '';
|
|
1619
|
+
[
|
|
1620
|
+
['shadingToonyFactor', 'toonToony', 0, 1, 0.01],
|
|
1621
|
+
['shadingShiftFactor', 'toonShift', -1, 1, 0.01],
|
|
1622
|
+
['shadeStrength', 'toonShade', 0, 1, 0.05],
|
|
1623
|
+
['outlineScale', 'toonOutline', 0, 3, 0.05],
|
|
1624
|
+
].forEach(function (spec) {
|
|
1625
|
+
var row = document.createElement('div'); row.className = 'row';
|
|
1626
|
+
var label = document.createElement('label'); label.className = 'k'; label.htmlFor = 'toon-' + spec[0]; label.textContent = t(spec[1]);
|
|
1627
|
+
var input = document.createElement('input'); input.type = 'number'; input.id = label.htmlFor;
|
|
1628
|
+
input.min = spec[2]; input.max = spec[3]; input.step = spec[4];
|
|
1629
|
+
input.value = prefs[spec[0]] === undefined ? '' : prefs[spec[0]];
|
|
1630
|
+
var original = materials.map(function (m) { return m[spec[0]]; });
|
|
1631
|
+
input.placeholder = spec[0] === 'shadeStrength' || spec[0] === 'outlineScale' ? '1'
|
|
1632
|
+
: original.length && original.every(function (v) { return v === original[0]; }) ? String(Number(original[0].toFixed(2))) : t('toonMixed');
|
|
1633
|
+
var noOutline = spec[0] === 'outlineScale' && !materials.some(function (m) { return m.outline; });
|
|
1634
|
+
input.disabled = !supported || prefs.enabled === false || noOutline;
|
|
1635
|
+
input.onchange = function () {
|
|
1636
|
+
if (input.value !== '' && !Number.isFinite(Number(input.value))) return;
|
|
1637
|
+
save(spec[0], input.value === '' ? undefined : Number(input.value));
|
|
1638
|
+
};
|
|
1639
|
+
row.appendChild(label); row.appendChild(input);
|
|
1640
|
+
var note = document.createElement('span'); note.className = 'note';
|
|
1641
|
+
note.textContent = noOutline ? t('toonNoOutline') : spec[2] + ' … ' + spec[3]; row.appendChild(note);
|
|
1642
|
+
controls.appendChild(row);
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1556
1646
|
function expressionRows() {
|
|
1557
1647
|
if (!modelInfo || !Array.isArray(modelInfo.expressions)) return [];
|
|
1558
1648
|
return modelInfo.expressions.slice().sort(function (left, right) {
|
|
@@ -4336,16 +4426,17 @@
|
|
|
4336
4426
|
* Both are SETTINGS rather than constants in the renderer (they were `GAZE_CURVE_EDGE_SLOPE` and a fixed
|
|
4337
4427
|
* span), because the right shape depends on the display and on the person: on a wide monitor the default
|
|
4338
4428
|
* span makes the head turn very little, and anyone who wants it livelier lowers the curve's edge slope
|
|
4339
|
-
* instead of reaching for a different pet. `0` in either span field means AUTO — the
|
|
4429
|
+
* instead of reaching for a different pet. `0` in either span field means AUTO — half the screen width —
|
|
4340
4430
|
* and the hint under the row says which number that resolves to for the floating window.
|
|
4341
4431
|
*/
|
|
4342
4432
|
function bindGazeControls() {
|
|
4343
|
-
var
|
|
4344
|
-
var curveValue = element('pref-gaze-curve-value');
|
|
4345
|
-
var curveReset = element('pref-gaze-curve-reset');
|
|
4433
|
+
var curveMode = element('pref-gaze-mode');
|
|
4346
4434
|
var spanX = element('pref-gaze-span-x');
|
|
4347
4435
|
var spanY = element('pref-gaze-span-y');
|
|
4348
4436
|
var spanReset = element('pref-gaze-span-reset');
|
|
4437
|
+
var pitchUp = element('pref-gaze-pitch-up');
|
|
4438
|
+
var pitchDown = element('pref-gaze-pitch-down');
|
|
4439
|
+
var pitchReset = element('pref-gaze-pitch-reset');
|
|
4349
4440
|
var idle = element('pref-gaze-idle');
|
|
4350
4441
|
var idleReset = element('pref-gaze-idle-reset');
|
|
4351
4442
|
// The draught's four numbers live in the same section — one place for everything that answers "how does the
|
|
@@ -4366,9 +4457,20 @@
|
|
|
4366
4457
|
persist();
|
|
4367
4458
|
};
|
|
4368
4459
|
var showCurve = function () {
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
if (
|
|
4460
|
+
if (curveMode) curveMode.value = preferences.gazeCurveMode;
|
|
4461
|
+
var radial = element('gaze-curves-radial');
|
|
4462
|
+
if (radial) radial.hidden = preferences.gazeCurveMode !== 'radial';
|
|
4463
|
+
var spatial = element('gaze-curves-spatial');
|
|
4464
|
+
if (spatial) spatial.hidden = preferences.gazeCurveMode !== 'spatial';
|
|
4465
|
+
var distance = element('pref-gaze-distance');
|
|
4466
|
+
if (distance) distance.value = String(preferences.gazeDistance);
|
|
4467
|
+
[['radial', 'gazeCurveRadial'], ['spatial', 'gazeCurveSpatial']].forEach(function (entry) {
|
|
4468
|
+
var control = element('pref-gaze-curve-' + entry[0]);
|
|
4469
|
+
var value = element('pref-gaze-curve-' + entry[0] + '-value');
|
|
4470
|
+
var current = preferences[entry[1]];
|
|
4471
|
+
if (control) control.value = String(Math.round(current * 100));
|
|
4472
|
+
if (value) value.textContent = current.toFixed(2) + (current >= 0.995 ? t('straightLine') : '');
|
|
4473
|
+
});
|
|
4372
4474
|
};
|
|
4373
4475
|
var showSpans = function () {
|
|
4374
4476
|
if (spanX) spanX.value = String(Math.round(Number(preferences.gazeSpanX) || 0));
|
|
@@ -4397,18 +4499,21 @@
|
|
|
4397
4499
|
if (windSpeed) windSpeed.value = String(Math.round(Number(preferences.windSpeedForMax)));
|
|
4398
4500
|
};
|
|
4399
4501
|
|
|
4400
|
-
if (
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4502
|
+
if (curveMode) curveMode.onchange = function () { write('gazeCurveMode', curveMode.value === 'radial' ? 'radial' : 'spatial'); showCurve(); };
|
|
4503
|
+
[['radial', 'gazeCurveRadial'], ['spatial', 'gazeCurveSpatial']].forEach(function (entry) {
|
|
4504
|
+
var control = element('pref-gaze-curve-' + entry[0]);
|
|
4505
|
+
var reset = element('pref-gaze-curve-' + entry[0] + '-reset');
|
|
4506
|
+
if (control) control.oninput = function () { write(entry[1], Number(control.value) / 100); showCurve(); };
|
|
4507
|
+
if (reset) reset.onclick = function () { write(entry[1], core.DEFAULTS[entry[1]]); showCurve(); };
|
|
4508
|
+
});
|
|
4509
|
+
var targetDistance = element('pref-gaze-distance');
|
|
4510
|
+
var targetDistanceReset = element('pref-gaze-distance-reset');
|
|
4511
|
+
if (targetDistance) targetDistance.onchange = function () {
|
|
4512
|
+
var value = targetDistance.valueAsNumber;
|
|
4513
|
+
write('gazeDistance', Number.isFinite(value) ? Math.max(0.1, Math.min(5, value)) : core.DEFAULTS.gazeDistance);
|
|
4514
|
+
showCurve();
|
|
4515
|
+
};
|
|
4516
|
+
if (targetDistanceReset) targetDistanceReset.onclick = function () { write('gazeDistance', core.DEFAULTS.gazeDistance); showCurve(); };
|
|
4412
4517
|
if (spanX) {
|
|
4413
4518
|
spanX.addEventListener('change', function () {
|
|
4414
4519
|
write('gazeSpanX', Math.max(0, Number(spanX.value) || 0));
|
|
@@ -4495,6 +4600,24 @@
|
|
|
4495
4600
|
});
|
|
4496
4601
|
}
|
|
4497
4602
|
showCurve();
|
|
4603
|
+
function showPitch() {
|
|
4604
|
+
if (pitchUp) pitchUp.value = String(Math.round(preferences.gazePitchUp * 10) / 10);
|
|
4605
|
+
if (pitchDown) pitchDown.value = String(Math.round(preferences.gazePitchDown * 10) / 10);
|
|
4606
|
+
}
|
|
4607
|
+
[[pitchUp, 'gazePitchUp'], [pitchDown, 'gazePitchDown']].forEach(function (entry) {
|
|
4608
|
+
if (!entry[0]) return;
|
|
4609
|
+
entry[0].onchange = function () {
|
|
4610
|
+
var value = entry[0].valueAsNumber;
|
|
4611
|
+
write(entry[1], Number.isFinite(value) ? Math.max(0, Math.min(60, value)) : core.DEFAULTS[entry[1]]);
|
|
4612
|
+
showPitch();
|
|
4613
|
+
};
|
|
4614
|
+
});
|
|
4615
|
+
function setPitch(up, down) {
|
|
4616
|
+
preferences.gazePitchUp = up; preferences.gazePitchDown = down;
|
|
4617
|
+
persist(); showPitch();
|
|
4618
|
+
}
|
|
4619
|
+
if (pitchReset) pitchReset.onclick = function () { setPitch(core.DEFAULTS.gazePitchUp, core.DEFAULTS.gazePitchDown); };
|
|
4620
|
+
showPitch();
|
|
4498
4621
|
showSpans();
|
|
4499
4622
|
showIdle();
|
|
4500
4623
|
showWind();
|
|
@@ -4533,7 +4656,8 @@
|
|
|
4533
4656
|
// work area, and the floating pet's horizontal full deflection IS that width.
|
|
4534
4657
|
var auto = element('pref-gaze-span-auto');
|
|
4535
4658
|
if (auto) {
|
|
4536
|
-
var work = status.
|
|
4659
|
+
var work = status.page && status.page.gaze && Number.isFinite(status.page.gaze.autoSpan) ? Math.round(status.page.gaze.autoSpan) :
|
|
4660
|
+
status.display && status.display.workArea ? Math.round(status.display.workArea.width / 2) : 0;
|
|
4537
4661
|
auto.textContent = work > 0 ? fill(t('spanNow'), { px: work }) : t('noScreenSize');
|
|
4538
4662
|
}
|
|
4539
4663
|
showAppliedGaze(status);
|
|
@@ -4564,6 +4688,8 @@
|
|
|
4564
4688
|
} else {
|
|
4565
4689
|
live.appliedGaze = {
|
|
4566
4690
|
curve: Number(gaze.curve),
|
|
4691
|
+
curveY: Number.isFinite(gaze.curveY) ? gaze.curveY : Number(gaze.curve),
|
|
4692
|
+
mode: gaze.mode === 'radial' ? 'radial' : 'spatial',
|
|
4567
4693
|
spanX: Math.round(Number(gaze.spanX) || 0),
|
|
4568
4694
|
spanY: Math.round(Number(gaze.spanY) || 0),
|
|
4569
4695
|
aim: { x: Number((gaze.aim && gaze.aim.x) || 0), y: Number((gaze.aim && gaze.aim.y) || 0) },
|
|
@@ -4581,7 +4707,9 @@
|
|
|
4581
4707
|
}
|
|
4582
4708
|
var aim = live.appliedGaze;
|
|
4583
4709
|
host.textContent = fill(t('gazeApplied'), {
|
|
4584
|
-
curve: aim.curve.toFixed(2)
|
|
4710
|
+
curve: aim.mode === 'spatial' ? t('gazeSpatial') + ' ' + aim.curve.toFixed(2)
|
|
4711
|
+
: aim.mode === 'radial' ? t('gazeRadial') + ' ' + aim.curve.toFixed(2)
|
|
4712
|
+
: t('gazeAxes') + ' ' + aim.curve.toFixed(2) + ' / ' + aim.curveY.toFixed(2),
|
|
4585
4713
|
spanX: aim.spanX,
|
|
4586
4714
|
spanY: aim.spanY,
|
|
4587
4715
|
aimX: aim.aim.x.toFixed(2),
|
|
@@ -5052,10 +5180,10 @@
|
|
|
5052
5180
|
* which it is. The resolved spans are what "auto" currently means, so the number is checkable.
|
|
5053
5181
|
*/
|
|
5054
5182
|
gaze: {
|
|
5055
|
-
controls: Boolean(element('pref-gaze-curve') && element('pref-gaze-span-x') && element('pref-gaze-span-y')),
|
|
5056
|
-
curve: Number(preferences.
|
|
5057
|
-
spanX: core.resolveGazeSpan(preferences.gazeSpanX, 0),
|
|
5058
|
-
spanY: core.resolveGazeSpan(preferences.gazeSpanY, 0),
|
|
5183
|
+
controls: Boolean(element('pref-gaze-mode') && element('pref-gaze-curve-spatial') && element('pref-gaze-curve-radial') && element('pref-gaze-span-x') && element('pref-gaze-span-y')),
|
|
5184
|
+
curve: Number(preferences.gazeCurveMode === 'radial' ? preferences.gazeCurveRadial : preferences.gazeCurveSpatial),
|
|
5185
|
+
spanX: core.resolveGazeSpan(preferences.gazeSpanX, Number(globalThis.screen && globalThis.screen.width) || 0),
|
|
5186
|
+
spanY: core.resolveGazeSpan(preferences.gazeSpanY, Number(globalThis.screen && globalThis.screen.width) || 0),
|
|
5059
5187
|
// The readout of what the RENDERER applies, and the numbers behind it: `applied` is null until the
|
|
5060
5188
|
// first /vrmpet3d/desktop answer lands, which is why a check asserts the element exists rather than a
|
|
5061
5189
|
// value that depends on a running window.
|