diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index d4bf441..0e91cf7 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -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++; diff --git a/3dtank_bundle.js b/3dtank_bundle.js index d4bf441..0e91cf7 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -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++; diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index 521fce3..aa1e4ee 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -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++;