fix(wireframe): 去掉炮塔门槛并加实体节点扫描兜底,修复只有点没有模型

This commit is contained in:
TermiNexus committed 2026-10-19 20:14:47 +08:00
1 parent dcf9a57456
commit 1dd7143e67
3 files changed
+262 -7

No files matched your search

@@ -1786,9 +1786,16 @@
}
// 炮塔 BP 根节点
// 新版:weaponSkin 组件(类 LUt,构造首字段 a11p_1)里 e11p_1 = CUt 包装,其 a17q_1 = 炮塔节点
function GetTurretNode(entity) {
var wp = FindComponentByField(entity, 'a11p_1');
if (wp) {
if (wp.e11p_1 && wp.e11p_1.a17q_1) return wp.e11p_1.a17q_1;
if (typeof wp.u11p === 'function') { try { return wp.u11p(); } catch (e) {} }
}
var comp = GetSkinComponent(entity);
return (comp && comp.a17q_1) ? comp.a17q_1 : null;
if (comp && comp.a17q_1) return comp.a17q_1;
return null;
}
// 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v)
@@ -1866,6 +1873,7 @@
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
var StrideDiagPrinted = new WeakSet();
var GpuFailLogged = new WeakSet();
function GetGpuData(tp) {
if (!tp) return null;
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
@@ -1907,6 +1915,33 @@
}
return data;
}
// 采集失败诊断(每个资源只打一次):直接指出卡在顶点、索引还是 GL 上下文
if (!GpuFailLogged.has(tp)) {
GpuFailLogged.add(tp);
var D = {};
try {
var G = tp.x4w_1 && typeof tp.x4w_1.g1 === 'function' ? tp.x4w_1.g1(0) : null;
var B = G && G.h3x_1;
var Dev = B && B.s3h_1;
var I = tp.y4w_1 && typeof tp.y4w_1.e1 === 'function' && tp.y4w_1.e1() > 0 ? tp.y4w_1.g1(0) : null;
var IB = I && (I.q4w_1 || I.u3p_1);
D = {
顶点组: tp.x4w_1 && tp.x4w_1.e1 ? tp.x4w_1.e1() : -1,
顶点缓冲资源: !!B,
缓冲包装: !!(B && B.v3x_1),
原生Buffer: !!(B && B.v3x_1 && B.v3x_1.d75_1),
设备: !!Dev,
原生GL: !!(Dev && Dev.f75_1),
顶点数: B ? B.w3x_1 : -1,
位置长度: positions ? positions.length : -1,
索引组: tp.y4w_1 && tp.y4w_1.e1 ? tp.y4w_1.e1() : -1,
索引资源: !!IB,
索引Buffer: !!(IB && IB.n3x_1 && IB.n3x_1.d75_1),
索引个数: indices ? indices.length : -1
};
} catch (e) {}
console.warn('[透视][GPU] 采集失败', D);
}
return null;
}
@@ -1956,6 +1991,47 @@
}
}
// 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 3D 节点
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
function IsSceneNode(V) {
return !!(V && typeof V === 'object' &&
typeof V.g35_1 === 'string' &&
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
V.r35_1 && typeof V.r35_1 === 'object');
}
function ScanMeshNodes(tank, nodes, stats, Seen) {
Seen = Seen || new WeakSet();
function Try(V) {
if (!IsSceneNode(V) || Seen.has(V)) return;
Seen.add(V);
CollectMeshNodes(V, nodes, 0, 'scan', stats);
}
try {
tank.qz1_1.s2a_1.forEach(function (VL) {
var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL;
if (!comp) return;
for (var K in comp) {
var V = comp[K];
if (!V || typeof V !== 'object') continue;
if (IsSceneNode(V)) { Try(V); continue; }
// 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1)
try {
for (var K2 in V) {
var V2 = V[K2];
if (!V2 || typeof V2 !== 'object') continue;
if (IsSceneNode(V2)) { Try(V2); continue; }
for (var K3 in V2) {
var V3 = V2[K3];
if (V3 && typeof V3 === 'object') Try(V3);
}
}
} catch (e) {}
}
});
} catch (e) {}
}
// ===== 提取坦克的mesh数据 =====
function GetTankMeshData(tank, stats) {
var nodes = [];
@@ -1984,6 +2060,13 @@
if (stats) stats.turretRoot = true;
try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {}
}
// 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点
if (!skin || !turretNode || nodes.length === 0) {
var Seen = new WeakSet();
for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); }
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
if (stats) stats.hullSource = (stats.hullSource || '扫描');
}
return nodes;
}
@@ -2591,7 +2674,9 @@
for (var gi = 0; gi < meshNodes.length; gi++) {
if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++;
}
if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记
// 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点
// (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉)
if (hullCount === 0 && turretCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
drawnCount++;
+87 -2
View File
@@ -1786,9 +1786,16 @@
}
// 炮塔 BP 根节点
// 新版:weaponSkin 组件(类 LUt,构造首字段 a11p_1)里 e11p_1 = CUt 包装,其 a17q_1 = 炮塔节点
function GetTurretNode(entity) {
var wp = FindComponentByField(entity, 'a11p_1');
if (wp) {
if (wp.e11p_1 && wp.e11p_1.a17q_1) return wp.e11p_1.a17q_1;
if (typeof wp.u11p === 'function') { try { return wp.u11p(); } catch (e) {} }
}
var comp = GetSkinComponent(entity);
return (comp && comp.a17q_1) ? comp.a17q_1 : null;
if (comp && comp.a17q_1) return comp.a17q_1;
return null;
}
// 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v)
@@ -1866,6 +1873,7 @@
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
var StrideDiagPrinted = new WeakSet();
var GpuFailLogged = new WeakSet();
function GetGpuData(tp) {
if (!tp) return null;
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
@@ -1907,6 +1915,33 @@
}
return data;
}
// 采集失败诊断(每个资源只打一次):直接指出卡在顶点、索引还是 GL 上下文
if (!GpuFailLogged.has(tp)) {
GpuFailLogged.add(tp);
var D = {};
try {
var G = tp.x4w_1 && typeof tp.x4w_1.g1 === 'function' ? tp.x4w_1.g1(0) : null;
var B = G && G.h3x_1;
var Dev = B && B.s3h_1;
var I = tp.y4w_1 && typeof tp.y4w_1.e1 === 'function' && tp.y4w_1.e1() > 0 ? tp.y4w_1.g1(0) : null;
var IB = I && (I.q4w_1 || I.u3p_1);
D = {
顶点组: tp.x4w_1 && tp.x4w_1.e1 ? tp.x4w_1.e1() : -1,
顶点缓冲资源: !!B,
缓冲包装: !!(B && B.v3x_1),
原生Buffer: !!(B && B.v3x_1 && B.v3x_1.d75_1),
设备: !!Dev,
原生GL: !!(Dev && Dev.f75_1),
顶点数: B ? B.w3x_1 : -1,
位置长度: positions ? positions.length : -1,
索引组: tp.y4w_1 && tp.y4w_1.e1 ? tp.y4w_1.e1() : -1,
索引资源: !!IB,
索引Buffer: !!(IB && IB.n3x_1 && IB.n3x_1.d75_1),
索引个数: indices ? indices.length : -1
};
} catch (e) {}
console.warn('[透视][GPU] 采集失败', D);
}
return null;
}
@@ -1956,6 +1991,47 @@
}
}
// 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 3D 节点
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
function IsSceneNode(V) {
return !!(V && typeof V === 'object' &&
typeof V.g35_1 === 'string' &&
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
V.r35_1 && typeof V.r35_1 === 'object');
}
function ScanMeshNodes(tank, nodes, stats, Seen) {
Seen = Seen || new WeakSet();
function Try(V) {
if (!IsSceneNode(V) || Seen.has(V)) return;
Seen.add(V);
CollectMeshNodes(V, nodes, 0, 'scan', stats);
}
try {
tank.qz1_1.s2a_1.forEach(function (VL) {
var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL;
if (!comp) return;
for (var K in comp) {
var V = comp[K];
if (!V || typeof V !== 'object') continue;
if (IsSceneNode(V)) { Try(V); continue; }
// 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1)
try {
for (var K2 in V) {
var V2 = V[K2];
if (!V2 || typeof V2 !== 'object') continue;
if (IsSceneNode(V2)) { Try(V2); continue; }
for (var K3 in V2) {
var V3 = V2[K3];
if (V3 && typeof V3 === 'object') Try(V3);
}
}
} catch (e) {}
}
});
} catch (e) {}
}
// ===== 提取坦克的mesh数据 =====
function GetTankMeshData(tank, stats) {
var nodes = [];
@@ -1984,6 +2060,13 @@
if (stats) stats.turretRoot = true;
try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {}
}
// 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点
if (!skin || !turretNode || nodes.length === 0) {
var Seen = new WeakSet();
for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); }
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
if (stats) stats.hullSource = (stats.hullSource || '扫描');
}
return nodes;
}
@@ -2591,7 +2674,9 @@
for (var gi = 0; gi < meshNodes.length; gi++) {
if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++;
}
if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记
// 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点
// (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉)
if (hullCount === 0 && turretCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
drawnCount++;
+88 -3
View File
@@ -261,9 +261,16 @@
}
// 炮塔 BP 根节点
// 新版:weaponSkin 组件(类 LUt,构造首字段 a11p_1)里 e11p_1 = CUt 包装,其 a17q_1 = 炮塔节点
function GetTurretNode(entity) {
var wp = FindComponentByField(entity, 'a11p_1');
if (wp) {
if (wp.e11p_1 && wp.e11p_1.a17q_1) return wp.e11p_1.a17q_1;
if (typeof wp.u11p === 'function') { try { return wp.u11p(); } catch (e) {} }
}
var comp = GetSkinComponent(entity);
return (comp && comp.a17q_1) ? comp.a17q_1 : null;
if (comp && comp.a17q_1) return comp.a17q_1;
return null;
}
// 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v)
@@ -340,7 +347,8 @@
var GpuCacheFrameCount = 0;
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
var StrideDiagPrinted = new WeakSet();
var StrideDiagPrinted = new WeakSet();
var GpuFailLogged = new WeakSet();
function GetGpuData(tp) {
if (!tp) return null;
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
@@ -382,6 +390,33 @@
}
return data;
}
// 采集失败诊断(每个资源只打一次):直接指出卡在顶点、索引还是 GL 上下文
if (!GpuFailLogged.has(tp)) {
GpuFailLogged.add(tp);
var D = {};
try {
var G = tp.x4w_1 && typeof tp.x4w_1.g1 === 'function' ? tp.x4w_1.g1(0) : null;
var B = G && G.h3x_1;
var Dev = B && B.s3h_1;
var I = tp.y4w_1 && typeof tp.y4w_1.e1 === 'function' && tp.y4w_1.e1() > 0 ? tp.y4w_1.g1(0) : null;
var IB = I && (I.q4w_1 || I.u3p_1);
D = {
顶点组: tp.x4w_1 && tp.x4w_1.e1 ? tp.x4w_1.e1() : -1,
顶点缓冲资源: !!B,
缓冲包装: !!(B && B.v3x_1),
原生Buffer: !!(B && B.v3x_1 && B.v3x_1.d75_1),
设备: !!Dev,
原生GL: !!(Dev && Dev.f75_1),
顶点数: B ? B.w3x_1 : -1,
位置长度: positions ? positions.length : -1,
索引组: tp.y4w_1 && tp.y4w_1.e1 ? tp.y4w_1.e1() : -1,
索引资源: !!IB,
索引Buffer: !!(IB && IB.n3x_1 && IB.n3x_1.d75_1),
索引个数: indices ? indices.length : -1
};
} catch (e) {}
console.warn('[透视][GPU] 采集失败', D);
}
return null;
}
@@ -431,6 +466,47 @@
}
}
// 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 3D 节点
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
function IsSceneNode(V) {
return !!(V && typeof V === 'object' &&
typeof V.g35_1 === 'string' &&
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
V.r35_1 && typeof V.r35_1 === 'object');
}
function ScanMeshNodes(tank, nodes, stats, Seen) {
Seen = Seen || new WeakSet();
function Try(V) {
if (!IsSceneNode(V) || Seen.has(V)) return;
Seen.add(V);
CollectMeshNodes(V, nodes, 0, 'scan', stats);
}
try {
tank.qz1_1.s2a_1.forEach(function (VL) {
var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL;
if (!comp) return;
for (var K in comp) {
var V = comp[K];
if (!V || typeof V !== 'object') continue;
if (IsSceneNode(V)) { Try(V); continue; }
// 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1)
try {
for (var K2 in V) {
var V2 = V[K2];
if (!V2 || typeof V2 !== 'object') continue;
if (IsSceneNode(V2)) { Try(V2); continue; }
for (var K3 in V2) {
var V3 = V2[K3];
if (V3 && typeof V3 === 'object') Try(V3);
}
}
} catch (e) {}
}
});
} catch (e) {}
}
// ===== 提取坦克的mesh数据 =====
function GetTankMeshData(tank, stats) {
var nodes = [];
@@ -459,6 +535,13 @@
if (stats) stats.turretRoot = true;
try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {}
}
// 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点
if (!skin || !turretNode || nodes.length === 0) {
var Seen = new WeakSet();
for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); }
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
if (stats) stats.hullSource = (stats.hullSource || '扫描');
}
return nodes;
}
@@ -1066,7 +1149,9 @@
for (var gi = 0; gi < meshNodes.length; gi++) {
if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++;
}
if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记
// 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点
// (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉)
if (hullCount === 0 && turretCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
drawnCount++;