diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index 21a50a3..8d77dce 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -2082,7 +2082,44 @@ } catch (e) {} } + // 结果级去重:同一 mesh 资源 + 同一世界平移,只保留一份 + // (引擎里同一逻辑节点可能存在「皮肤树 / 场景树」两套实例,AABB/矩阵更新时机不同, + // 两套都画会出现「复制出一个不动的身体,过一会才跟上」) + var TpIds = new WeakMap(); + var TpNextId = 1; + function DedupNodes(nodes) { + if (nodes.length < 2) return nodes; + var Keys = new Set(); + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + var Tp = N.tp; + var Id = TpIds.get(Tp); + if (!Id) { Id = TpNextId++; TpIds.set(Tp, Id); } + var M = N.node && N.node.p35_1; + var Pos = M ? (Math.round(M[12]) + ',' + Math.round(M[13]) + ',' + Math.round(M[14])) : '?'; + var Key = Id + ':' + N.name + ':' + Pos; + if (Keys.has(Key)) continue; + Keys.add(Key); + Out.push(N); + } + return Out; + } + + // 扫描节流:仅在主路径完全采不到时使用,且同一辆坦克至少间隔 10 帧 + var ScanTick = 0; + var LastScanAt = new WeakMap(); + function ShouldScanNow(tank) { + ScanTick++; + var Last = LastScanAt.get(tank) || 0; + if (ScanTick - Last < 10) return false; + LastScanAt.set(tank, ScanTick); + return true; + } + // ===== 提取坦克的mesh数据 ===== + // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") + // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(节流兜底) function GetTankMeshData(tank, stats) { var nodes = []; var Seen = new WeakSet(); @@ -2092,12 +2129,12 @@ stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; } - // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + // ① 主路径:车身根节点(包含全部部件节点) if (skinComp && skinComp.h159_1) { try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } - // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) - if (skin) { + // ② 根节点采不到才退回部件表 + if (nodes.length === 0 && skin) { try { ForEachInList(skin.g17r_1, function (part) { if (!part) return; @@ -2113,16 +2150,18 @@ }); } catch(e) {} } + // ③ 仍然为空才做组件字段扫描(节流,避免每帧全量遍历造成卡顿) + if (nodes.length === 0 && ShouldScanNow(tank)) { + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + } + // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } - // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 - // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - return nodes; + return DedupNodes(nodes); } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 ===== diff --git a/3dtank_bundle.js b/3dtank_bundle.js index 21a50a3..8d77dce 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -2082,7 +2082,44 @@ } catch (e) {} } + // 结果级去重:同一 mesh 资源 + 同一世界平移,只保留一份 + // (引擎里同一逻辑节点可能存在「皮肤树 / 场景树」两套实例,AABB/矩阵更新时机不同, + // 两套都画会出现「复制出一个不动的身体,过一会才跟上」) + var TpIds = new WeakMap(); + var TpNextId = 1; + function DedupNodes(nodes) { + if (nodes.length < 2) return nodes; + var Keys = new Set(); + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + var Tp = N.tp; + var Id = TpIds.get(Tp); + if (!Id) { Id = TpNextId++; TpIds.set(Tp, Id); } + var M = N.node && N.node.p35_1; + var Pos = M ? (Math.round(M[12]) + ',' + Math.round(M[13]) + ',' + Math.round(M[14])) : '?'; + var Key = Id + ':' + N.name + ':' + Pos; + if (Keys.has(Key)) continue; + Keys.add(Key); + Out.push(N); + } + return Out; + } + + // 扫描节流:仅在主路径完全采不到时使用,且同一辆坦克至少间隔 10 帧 + var ScanTick = 0; + var LastScanAt = new WeakMap(); + function ShouldScanNow(tank) { + ScanTick++; + var Last = LastScanAt.get(tank) || 0; + if (ScanTick - Last < 10) return false; + LastScanAt.set(tank, ScanTick); + return true; + } + // ===== 提取坦克的mesh数据 ===== + // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") + // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(节流兜底) function GetTankMeshData(tank, stats) { var nodes = []; var Seen = new WeakSet(); @@ -2092,12 +2129,12 @@ stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; } - // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + // ① 主路径:车身根节点(包含全部部件节点) if (skinComp && skinComp.h159_1) { try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } - // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) - if (skin) { + // ② 根节点采不到才退回部件表 + if (nodes.length === 0 && skin) { try { ForEachInList(skin.g17r_1, function (part) { if (!part) return; @@ -2113,16 +2150,18 @@ }); } catch(e) {} } + // ③ 仍然为空才做组件字段扫描(节流,避免每帧全量遍历造成卡顿) + if (nodes.length === 0 && ShouldScanNow(tank)) { + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + } + // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } - // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 - // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - return nodes; + return DedupNodes(nodes); } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 ===== diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index f4b54ee..ae97ecd 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -557,7 +557,44 @@ } catch (e) {} } + // 结果级去重:同一 mesh 资源 + 同一世界平移,只保留一份 + // (引擎里同一逻辑节点可能存在「皮肤树 / 场景树」两套实例,AABB/矩阵更新时机不同, + // 两套都画会出现「复制出一个不动的身体,过一会才跟上」) + var TpIds = new WeakMap(); + var TpNextId = 1; + function DedupNodes(nodes) { + if (nodes.length < 2) return nodes; + var Keys = new Set(); + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + var Tp = N.tp; + var Id = TpIds.get(Tp); + if (!Id) { Id = TpNextId++; TpIds.set(Tp, Id); } + var M = N.node && N.node.p35_1; + var Pos = M ? (Math.round(M[12]) + ',' + Math.round(M[13]) + ',' + Math.round(M[14])) : '?'; + var Key = Id + ':' + N.name + ':' + Pos; + if (Keys.has(Key)) continue; + Keys.add(Key); + Out.push(N); + } + return Out; + } + + // 扫描节流:仅在主路径完全采不到时使用,且同一辆坦克至少间隔 10 帧 + var ScanTick = 0; + var LastScanAt = new WeakMap(); + function ShouldScanNow(tank) { + ScanTick++; + var Last = LastScanAt.get(tank) || 0; + if (ScanTick - Last < 10) return false; + LastScanAt.set(tank, ScanTick); + return true; + } + // ===== 提取坦克的mesh数据 ===== + // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") + // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(节流兜底) function GetTankMeshData(tank, stats) { var nodes = []; var Seen = new WeakSet(); @@ -567,12 +604,12 @@ stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; } - // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + // ① 主路径:车身根节点(包含全部部件节点) if (skinComp && skinComp.h159_1) { try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } - // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) - if (skin) { + // ② 根节点采不到才退回部件表 + if (nodes.length === 0 && skin) { try { ForEachInList(skin.g17r_1, function (part) { if (!part) return; @@ -588,16 +625,18 @@ }); } catch(e) {} } + // ③ 仍然为空才做组件字段扫描(节流,避免每帧全量遍历造成卡顿) + if (nodes.length === 0 && ShouldScanNow(tank)) { + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + } + // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } - // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 - // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - return nodes; + return DedupNodes(nodes); } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 =====