dsh-plugin-vrmpet3d 0.1.1 → 0.1.2
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/desktop/main.js +2 -1
- package/desktop/preload.js +1 -0
- package/lib/client.core.js +279 -41
- package/lib/client.js +114 -16
- package/lib/client.menu.js +14 -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 +158 -40
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +34 -4
- package/lib/index.js +46 -25
- package/lib/menu.js +14 -4
- package/lib/scene.js +134 -7
- package/lib/vrm-info.js +16 -1
- package/package.json +1 -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.
|