dsh-pet-live2d 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +4 -0
- package/lib/client.js +182 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 2.3.1
|
|
4
|
+
|
|
5
|
+
### 修:摸头会出摸尾巴的效果
|
|
6
|
+
|
|
7
|
+
用户报的:"现在摸头也是出现的摸尾巴的效果,而且这是 live2d 会动的,这个判断网格似乎是静态的。"
|
|
8
|
+
|
|
9
|
+
两个原因叠在一起:
|
|
10
|
+
|
|
11
|
+
1. **这只宠物的尾巴/翅膀是三选一的可选配件**(狐狸尾 / 猫尾 / 狼尾 / 天使翅膀…),
|
|
12
|
+
同一时刻只有一个显形,其余**几何还留在原地** → 判定区域会重叠。实测 1112 个头部
|
|
13
|
+
命中点里有 **697 个同时命中尾巴**。
|
|
14
|
+
2. 点击路由原来**先判尾巴、再判头** → 重叠区的点击一律算摸尾巴。
|
|
15
|
+
|
|
16
|
+
现在**摸头优先于摸尾巴**:点头部一定给摸头的台词/反应,只有"没命中头部"才看尾巴。
|
|
17
|
+
|
|
18
|
+
顺带把一条**太弱的测试**补强:原来只断言"存在只命中尾巴的点"(在 697 点重叠下依然是绿的),
|
|
19
|
+
现在断的是**路由**(点头部给的必须是摸头的台词)—— 这才是用户实际遇到的问题。
|
|
20
|
+
|
|
21
|
+
### 关于"判断网格似乎是静态的"
|
|
22
|
+
|
|
23
|
+
查证结果(都有读数支撑):
|
|
24
|
+
|
|
25
|
+
- **头部判定是跟着动的**:同一网格隔 2.5 秒重采,命中格子会变(模型呼吸/摆动)。
|
|
26
|
+
每次点击都是现读**当前帧**的顶点,不是缓存的网格。
|
|
27
|
+
- **尾巴/翅膀的几何在没戴配件时是退化的**(缩成一点 → 被面积下限排掉),所以
|
|
28
|
+
"摸尾巴"在没有尾巴的造型下**本来就该没反应**——你之前能看到反应,是模型当时正戴着
|
|
29
|
+
翅膀/尾巴。测试里现在有一条**一致性断言**把这件事写死:有几何就该有命中,
|
|
30
|
+
几何全退化就该一个都不中。
|
|
31
|
+
- 真正"静态"的是**点击穿透用的那层粗网格遮罩**(剪影),它按定时重建、粒度也粗 ——
|
|
32
|
+
它只决定"这一下算不算落在角色身上",不参与摸头/摸尾巴的判定。
|
|
33
|
+
|
|
34
|
+
> 试过用透明度过滤隐藏配件,结果**把真尾巴一起滤掉了**(`visibleDrawables: 4/11`):
|
|
35
|
+
> 隐藏是靠缩放、而 `opacities` 表在渲染期未必是最终值,拿它当闸门会误杀正在显形的那一个。
|
|
36
|
+
> 所以放弃了这条思路,改用"路由优先级"解决重叠 —— 保留的诊断读口
|
|
37
|
+
> `partsDebug()` / `partHitCounts()` 仍会报"过滤前 vs 过滤后",方便下次判断。
|
|
38
|
+
|
|
3
39
|
## 2.3.0
|
|
4
40
|
|
|
5
41
|
### 摸头判定改成按模型自己的几何(不再是手绘方框)
|
package/README.md
CHANGED
|
@@ -124,6 +124,10 @@ dsh plugin --profile web add "link:./dsh-pet-live2d/dsh-live2d-pet"
|
|
|
124
124
|
|
|
125
125
|
- 摸头/摸尾巴按**三角面**判定:圆顶带耳朵尖的头部轮廓、尾巴的边缘都对得上,不是方框。
|
|
126
126
|
顶点是**当前帧变形后**的,所以头歪着、身体摆着也准。
|
|
127
|
+
- **摸头优先于摸尾巴**:这两个区域可能有重叠(尾巴/翅膀挂件在身后,几何上会伸到头部
|
|
128
|
+
附近),重叠处算摸头。
|
|
129
|
+
- **尾巴/翅膀是可选配件**:没戴的时候它们的几何是退化的,"摸尾巴"就是没反应 ——
|
|
130
|
+
这是对的,不是坏了。戴上翅膀/尾巴(装扮槽)之后才摸得到。
|
|
127
131
|
- 转圈看的是**累计转角**(不是位置绕了几圈),所以贴着角色转小圈、远远转大圈都算。
|
|
128
132
|
- 三个互动各自可开关,反应也能改(见下)。共用的三句台词在「气泡」设置里。
|
|
129
133
|
|
package/lib/client.js
CHANGED
|
@@ -289,7 +289,7 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
289
289
|
let tailIndices = null;
|
|
290
290
|
|
|
291
291
|
/**
|
|
292
|
-
* 部件 id → drawable
|
|
292
|
+
* 部件 id → drawable 下标(带所属部件下标):cdi3 的部件名 → 引擎原始表。
|
|
293
293
|
*
|
|
294
294
|
* 为什么不直接用部件 id 去 `getDrawableIndex()`:cdi3 的 `Parts` 是**部件** id
|
|
295
295
|
* (`Part46`、`neck_m` 这种),而那个 API 认的是 **drawable** id(`lianhong`、
|
|
@@ -300,7 +300,10 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
300
300
|
* "哪些 drawable 属于作者命名为头/脸/眼/眉/嘴/耳/发(或尾/翅/鳍)的那些部件" ——
|
|
301
301
|
* 用的是作者自己的分类,不是猜名字。
|
|
302
302
|
*
|
|
303
|
-
*
|
|
303
|
+
* 返回的是 `[{ index, part }]`:`part` 留着,因为**判定时要按部件透明度过滤**
|
|
304
|
+
* (见 isDrawableVisible)—— 隐藏的配件几何还在原地。
|
|
305
|
+
*
|
|
306
|
+
* @returns {Array<{index:number, part:number}>|null} 表结构不认识时返回 null
|
|
304
307
|
*/
|
|
305
308
|
const drawableIndicesForParts = (parts) => {
|
|
306
309
|
const raw = model?.internalModel?.coreModel?._model;
|
|
@@ -315,11 +318,38 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
315
318
|
const partIndex = parent[i];
|
|
316
319
|
if (partIndex < 0 || partIndex >= ids.length) continue;
|
|
317
320
|
if (!wanted.has(ids[partIndex])) continue;
|
|
318
|
-
out.push(i);
|
|
321
|
+
out.push({ index: i, part: partIndex });
|
|
319
322
|
}
|
|
320
323
|
return out.length > 0 ? out : null;
|
|
321
324
|
};
|
|
322
325
|
|
|
326
|
+
/**
|
|
327
|
+
* 这个 drawable 现在"看得见"吗?
|
|
328
|
+
*
|
|
329
|
+
* **注意:判定里不拿它当闸门。** 我试过用透明度过滤隐藏配件,结果是"真尾巴一起
|
|
330
|
+
* 被滤掉"—— 这只宠物的尾巴/翅膀全是**可选配件**,同一时刻只有一个显形,其余靠
|
|
331
|
+
* **缩放成一点**藏起来(被面积下限排掉就够了);而 `opacities` 表在渲染期未必是
|
|
332
|
+
* 最终值,拿它当闸门会误杀正在显形的那一个。
|
|
333
|
+
*
|
|
334
|
+
* 现在只留给诊断用(`partsDebug` / `partHitCounts` 报"过滤前 vs 过滤后")。
|
|
335
|
+
* "隐藏翅膀抢走摸头"这个真问题改由**路由优先级**解决:摸头优先于摸尾巴。
|
|
336
|
+
*/
|
|
337
|
+
const isDrawableVisible = (entry) => {
|
|
338
|
+
const im = model?.internalModel;
|
|
339
|
+
try {
|
|
340
|
+
if (typeof im?.getDrawableDynamicFlagIsVisible === "function"
|
|
341
|
+
&& im.getDrawableDynamicFlagIsVisible(entry.index) === false) return false;
|
|
342
|
+
} catch {
|
|
343
|
+
/* 没有这个 API 就继续看透明度 */
|
|
344
|
+
}
|
|
345
|
+
const raw = im?.coreModel?._model;
|
|
346
|
+
const drawOpacity = raw?.drawables?.opacities?.[entry.index];
|
|
347
|
+
if (typeof drawOpacity === "number" && drawOpacity <= 0.001) return false;
|
|
348
|
+
const partOpacity = raw?.parts?.opacities?.[entry.part];
|
|
349
|
+
if (typeof partOpacity === "number" && partOpacity <= 0.001) return false;
|
|
350
|
+
return true;
|
|
351
|
+
};
|
|
352
|
+
|
|
323
353
|
/**
|
|
324
354
|
* 点在不在这些 drawable 的**三角面**里(模型空间)。
|
|
325
355
|
*
|
|
@@ -346,8 +376,8 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
346
376
|
if (indices === null) return null;
|
|
347
377
|
let read = false;
|
|
348
378
|
try {
|
|
349
|
-
for (const
|
|
350
|
-
const verts = verticesOf(index);
|
|
379
|
+
for (const entry of indices) {
|
|
380
|
+
const verts = verticesOf(entry.index);
|
|
351
381
|
if (verts === undefined || verts === null || verts.length < 6) continue;
|
|
352
382
|
read = true;
|
|
353
383
|
// 先用这个 drawable 的包围盒排除(绝大多数部件一眼就出局,不用扫三角形)。
|
|
@@ -1936,22 +1966,158 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1936
1966
|
.filter((name) => typeof target?.[name] === "function");
|
|
1937
1967
|
let vertexProbe = "n/a";
|
|
1938
1968
|
try {
|
|
1939
|
-
const v = im?.getDrawableVertices?.(headIndices?.[0] ?? 0);
|
|
1969
|
+
const v = im?.getDrawableVertices?.(headIndices?.[0]?.index ?? 0);
|
|
1940
1970
|
vertexProbe = v === undefined ? "undefined" : (v === null ? "null" : "len=" + v.length);
|
|
1941
1971
|
} catch (error) {
|
|
1942
1972
|
vertexProbe = "throw: " + String(error?.message ?? error);
|
|
1943
1973
|
}
|
|
1974
|
+
const visible = (list) => (list === null ? 0 : list.filter((entry) => isDrawableVisible(entry)).length);
|
|
1944
1975
|
return {
|
|
1945
1976
|
parts: headParts.length,
|
|
1946
1977
|
drawableIndices: headIndices === null ? 0 : headIndices.length,
|
|
1978
|
+
headVisible: visible(headIndices),
|
|
1947
1979
|
tailParts: tailParts.length,
|
|
1948
1980
|
tailDrawableIndices: tailIndices === null ? 0 : tailIndices.length,
|
|
1981
|
+
tailVisible: visible(tailIndices),
|
|
1949
1982
|
apiOnInternalModel: has(im),
|
|
1950
1983
|
apiOnCoreModel: has(core),
|
|
1951
1984
|
vertexProbe,
|
|
1952
1985
|
box: headBox,
|
|
1953
1986
|
};
|
|
1954
1987
|
},
|
|
1988
|
+
/**
|
|
1989
|
+
* Diagnostic: 逐部件的透明度 / 几何范围 / drawable 数。
|
|
1990
|
+
*
|
|
1991
|
+
* "摸头顺带把摸尾巴也触发"这类问题要先分清是**隐藏的配件**(同一时刻只有一个
|
|
1992
|
+
* 显形:狐狸尾 / 猫尾 / 狼尾 / 天使翅膀…)几何还留在原地,还是判定本身写错了。
|
|
1993
|
+
*
|
|
1994
|
+
* **必须挂在控制器上**:`tailParts` 是这一层的闭包变量,放到组件的 api 里会
|
|
1995
|
+
* `tailParts is not defined`(我就这么错过一次,调用直接抛异常、外面只看到 undefined)。
|
|
1996
|
+
*/
|
|
1997
|
+
partsDebug: (which) => {
|
|
1998
|
+
const list = which === "tail" ? tailParts : headParts;
|
|
1999
|
+
const raw = model?.internalModel?.coreModel?._model;
|
|
2000
|
+
const im = model?.internalModel;
|
|
2001
|
+
const partIds = raw?.parts?.ids === undefined ? [] : Array.from(raw.parts.ids).map(String);
|
|
2002
|
+
const parent = raw?.drawables?.parentPartIndices;
|
|
2003
|
+
const out = [];
|
|
2004
|
+
for (const id of list) {
|
|
2005
|
+
const index = partIds.indexOf(id);
|
|
2006
|
+
const opacity = raw?.parts?.opacities?.[index];
|
|
2007
|
+
let minX = Infinity;
|
|
2008
|
+
let minY = Infinity;
|
|
2009
|
+
let maxX = -Infinity;
|
|
2010
|
+
let maxY = -Infinity;
|
|
2011
|
+
let drawables = 0;
|
|
2012
|
+
let visibleDrawables = 0;
|
|
2013
|
+
if (parent !== undefined) {
|
|
2014
|
+
for (let i = 0; i < parent.length; i += 1) {
|
|
2015
|
+
if (parent[i] !== index) continue;
|
|
2016
|
+
drawables += 1;
|
|
2017
|
+
if (isDrawableVisible({ index: i, part: index })) visibleDrawables += 1;
|
|
2018
|
+
try {
|
|
2019
|
+
const verts = im?.getDrawableVertices?.(i);
|
|
2020
|
+
if (verts === undefined || verts === null) continue;
|
|
2021
|
+
for (let k = 0; k < verts.length; k += 2) {
|
|
2022
|
+
if (verts[k] < minX) minX = verts[k];
|
|
2023
|
+
if (verts[k] > maxX) maxX = verts[k];
|
|
2024
|
+
if (verts[k + 1] < minY) minY = verts[k + 1];
|
|
2025
|
+
if (verts[k + 1] > maxY) maxY = verts[k + 1];
|
|
2026
|
+
}
|
|
2027
|
+
} catch {
|
|
2028
|
+
/* 读不到就跳过 */
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
out.push({
|
|
2033
|
+
id,
|
|
2034
|
+
partIndex: index,
|
|
2035
|
+
opacity: typeof opacity === "number" ? Math.round(opacity * 1000) / 1000 : null,
|
|
2036
|
+
drawables,
|
|
2037
|
+
visibleDrawables,
|
|
2038
|
+
box: maxX > minX
|
|
2039
|
+
? { minX: Math.round(minX), maxX: Math.round(maxX), minY: Math.round(minY), maxY: Math.round(maxY) }
|
|
2040
|
+
: null,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
return out;
|
|
2044
|
+
},
|
|
2045
|
+
/**
|
|
2046
|
+
* Diagnostic: 每个部件在模型空间的命中点数(过滤前 / 过滤后)+ 它的 drawable
|
|
2047
|
+
* 透明度范围。
|
|
2048
|
+
*
|
|
2049
|
+
* 采样**直接在模型空间**做,不经过 `vendor.Point` —— 第一版在 api 闭包里用了
|
|
2050
|
+
* `vendor`(那是控制器里的变量,这里根本看不到),每次调用都抛异常被 catch 吞掉,
|
|
2051
|
+
* 于是所有部件都报 0,看着像"这个部件完全没几何"。
|
|
2052
|
+
*/
|
|
2053
|
+
partHitCounts: (which, cols = 48, rows = 36) => {
|
|
2054
|
+
const parts = which === "tail" ? tailParts : headParts;
|
|
2055
|
+
const im = model?.internalModel;
|
|
2056
|
+
const raw = im?.coreModel?._model;
|
|
2057
|
+
const partIds = raw?.parts?.ids === undefined ? [] : Array.from(raw.parts.ids).map(String);
|
|
2058
|
+
const parent = raw?.drawables?.parentPartIndices;
|
|
2059
|
+
const out = [];
|
|
2060
|
+
for (const id of parts) {
|
|
2061
|
+
const partIndex = partIds.indexOf(id);
|
|
2062
|
+
const entries = [];
|
|
2063
|
+
if (parent !== undefined) {
|
|
2064
|
+
for (let i = 0; i < parent.length; i += 1) {
|
|
2065
|
+
if (partIndex >= 0) {
|
|
2066
|
+
if (parent[i] === partIndex) entries.push({ index: i, part: partIndex });
|
|
2067
|
+
} else if (typeof im?.getDrawableIndex === "function" && im.getDrawableIndex(id) === i) {
|
|
2068
|
+
// id 不在 parts.ids 里(它其实是个 drawable id):按 drawable 找它自己。
|
|
2069
|
+
entries.push({ index: i, part: -1 });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const opacities = [];
|
|
2074
|
+
let minX = Infinity;
|
|
2075
|
+
let minY = Infinity;
|
|
2076
|
+
let maxX = -Infinity;
|
|
2077
|
+
let maxY = -Infinity;
|
|
2078
|
+
for (const entry of entries) {
|
|
2079
|
+
const value = raw?.drawables?.opacities?.[entry.index];
|
|
2080
|
+
if (typeof value === "number") opacities.push(Math.round(value * 1000) / 1000);
|
|
2081
|
+
try {
|
|
2082
|
+
const verts = im?.getDrawableVertices?.(entry.index);
|
|
2083
|
+
if (verts === undefined || verts === null) continue;
|
|
2084
|
+
for (let k = 0; k < verts.length; k += 2) {
|
|
2085
|
+
if (verts[k] < minX) minX = verts[k];
|
|
2086
|
+
if (verts[k] > maxX) maxX = verts[k];
|
|
2087
|
+
if (verts[k + 1] < minY) minY = verts[k + 1];
|
|
2088
|
+
if (verts[k + 1] > maxY) maxY = verts[k + 1];
|
|
2089
|
+
}
|
|
2090
|
+
} catch {
|
|
2091
|
+
/* 跳过 */
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
let hitsAll = 0;
|
|
2095
|
+
let hitsVisible = 0;
|
|
2096
|
+
if (entries.length > 0 && maxX > minX && maxY > minY) {
|
|
2097
|
+
const visible = entries.filter((entry) => isDrawableVisible(entry));
|
|
2098
|
+
const pad = 20;
|
|
2099
|
+
for (let iy = 0; iy < rows; iy += 1) {
|
|
2100
|
+
for (let ix = 0; ix < cols; ix += 1) {
|
|
2101
|
+
const px = (minX - pad) + (maxX - minX + pad * 2) * (ix + 0.5) / cols;
|
|
2102
|
+
const py = (minY - pad) + (maxY - minY + pad * 2) * (iy + 0.5) / rows;
|
|
2103
|
+
if (hitsPartsGeometry(entries, px, py) === true) hitsAll += 1;
|
|
2104
|
+
if (visible.length > 0 && hitsPartsGeometry(visible, px, py) === true) hitsVisible += 1;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
out.push({
|
|
2109
|
+
id,
|
|
2110
|
+
partIndex,
|
|
2111
|
+
drawables: entries.length,
|
|
2112
|
+
opacityMin: opacities.length === 0 ? null : Math.min(...opacities),
|
|
2113
|
+
opacityMax: opacities.length === 0 ? null : Math.max(...opacities),
|
|
2114
|
+
box: maxX > minX ? { minX: Math.round(minX), maxX: Math.round(maxX), minY: Math.round(minY), maxY: Math.round(maxY) } : null,
|
|
2115
|
+
hitsAll,
|
|
2116
|
+
hitsVisible,
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
return out;
|
|
2120
|
+
},
|
|
1955
2121
|
/**
|
|
1956
2122
|
* Play a motion with its declared policy applied; used by the panel, the
|
|
1957
2123
|
* tap reaction and the session-phase driver.
|
|
@@ -5387,19 +5553,22 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
5387
5553
|
});
|
|
5388
5554
|
} else if (state.onModel) {
|
|
5389
5555
|
lastInteraction.current = Date.now();
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
if (list.length > 0) runReactionRef.current(pick(list));
|
|
5395
|
-
say(pick(linesNow().tail));
|
|
5396
|
-
} else if (state.onHead && FLAGS.patEnabled) {
|
|
5556
|
+
// **摸头优先于摸尾巴**:两者区域可能重叠(尾巴/翅膀挂件在身后,几何上会伸到
|
|
5557
|
+
// 头部附近),先判头才符合直觉 —— 用户报的"摸头出的是摸尾巴的效果"就是
|
|
5558
|
+
// 原来先判尾巴造成的。隐藏配件的问题另有 isDrawableVisible 兜着。
|
|
5559
|
+
if (state.onHead && FLAGS.patEnabled) {
|
|
5397
5560
|
// 摸头:从 `patReactions` 里随机挑一个(默认是 重锤出击 / 问号 / 星星眼),
|
|
5398
5561
|
// 并且**故意不脸红**。表情类反应是"闪一下",到点由自动清理收走,
|
|
5399
5562
|
// 所以摸头不会在用户选的槽位上留下永久表情。
|
|
5400
5563
|
const list = interactionReactions("patReactions");
|
|
5401
5564
|
if (list.length > 0) runReactionRef.current(pick(list));
|
|
5402
5565
|
say(pick(linesNow().pat));
|
|
5566
|
+
} else if (state.onTail && FLAGS.tailEnabled) {
|
|
5567
|
+
// 摸尾巴:判据和摸头同一套(模型自己的三角面),部件集合是 cdi3 里
|
|
5568
|
+
// 命名为尾/鳍/翅/翼 的那些。反应从 pet.json 的 `tailReactions` 里随机。
|
|
5569
|
+
const list = interactionReactions("tailReactions");
|
|
5570
|
+
if (list.length > 0) runReactionRef.current(pick(list));
|
|
5571
|
+
say(pick(linesNow().tail));
|
|
5403
5572
|
} else {
|
|
5404
5573
|
// Anywhere else on the character is a lighter acknowledgement —
|
|
5405
5574
|
// deliberately WITHOUT 重锤出击, which now belongs to the head only.
|