// ===== 3DTank3D透视 - 线框/描边双模式 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在全屏叠加层上绘制敌方坦克的3D模型 // 模式: // wireframe - 完整线框(绘制所有边) // outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点) // 切换:window.__hornet3DWireframe__.setMode('outline') (function() { function _start() { console.log('===== 3DTank3D透视启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出叠加层与状态行,模型等进入战局再出 if (window.__hornet3DWireframe__) { window.__hornet3DWireframe__.stop(); console.log('已停止旧透视'); } if (window.__hornet3DOutline__) { window.__hornet3DOutline__.stop(); console.log('已停止旧描边透视'); } var gm = window.__gameMode__ || null; // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { var __savedMode = localStorage.getItem('hornet_wireframe_mode'); if (__savedMode === 'wireframe' || __savedMode === 'outline') Mode = __savedMode; } catch(e) {} // ===== 线宽设置 ===== var LineWidth = 2; try { var __savedLW = localStorage.getItem('hornet_wireframe_linewidth'); if (__savedLW) { var __n = parseFloat(__savedLW); if (!isNaN(__n) && __n >= 0.5 && __n <= 8) LineWidth = __n; } } catch(e) {} // ===== 渲染模式 ===== // 'team' - 队伍模式:所有坦克按队伍渲染(蓝=友方,红=敌方) // 'whitelist' - 白名单:只有名单中的坦克渲染(按队伍颜色),其他不渲染 // 'blacklist' - 黑名单:名单中的坦克不渲染,其他按队伍颜色渲染 var HighlightMode = 'team'; try { var __savedHM = localStorage.getItem('hornet_wireframe_highlight_mode'); if (__savedHM && ['team','whitelist','blacklist'].indexOf(__savedHM) >= 0) { HighlightMode = __savedHM; } } catch(e) {} // ===== 名单 ===== var HighlightList = []; try { var __savedHL = localStorage.getItem('hornet_highlight_list'); if (__savedHL) HighlightList = JSON.parse(__savedHL); } catch(e) {} function SaveHighlightList() { try { localStorage.setItem('hornet_highlight_list', JSON.stringify(HighlightList)); } catch(e) {} } function IsInList(name) { if (!name) return false; for (var i = 0; i < HighlightList.length; i++) { if (HighlightList[i] === name) return true; } return false; } // 判断坦克是否应渲染(根据 HighlightMode) function ShouldRender(name) { switch (HighlightMode) { case 'team': return true; // 全部渲染 case 'whitelist': return IsInList(name); // 名单内渲染 case 'blacklist': return !IsInList(name); // 名单外渲染 default: return true; } } // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[透视] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { // 自由视角激活时,用虚拟相机参数(位置/方向跟随鼠标) var fl = window.__hornetFreeLook__; if (fl && typeof fl.IsActive === 'function' && fl.IsActive()) { var pos = fl.GetPosition(); var dir = fl.GetDirection(); if (pos && dir) { return { pos: pos, yaw: dir.yaw, pitch: dir.pitch, fov: 1.0471976 }; } } return lib.GetCameraInfo(gm); } var Project3D = lib.Project3D; var Project3DMatrix = lib.Project3DMatrix; // 优先用相机矩阵投影(新版唯一可靠路径);自由视角虚拟相机没有矩阵时退回欧拉角投影 function ProjectPoint(pos, camInfo, screenW, screenH) { if (camInfo && camInfo.matrix) { var r = Project3DMatrix(camInfo, pos, screenW, screenH); if (r) return r; } return Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); } // 人机标记:同队内装备分数相同且 < 8000(lib.ScanBots 定期扫描 → IsBot 查询) var ScanBots = lib.ScanBots, IsBot = lib.IsBot; var LastBotScanAt = 0; // 人机扫描节流(约每 0.3 秒一次) // ===== 热点优化:相机基每帧只算一次(避免逐顶点重复三角函数 + 对象分配)===== var Cam = { fx: 0, fy: 0, fz: 0, rx: 0, ry: 0, rz: 0, ux: 0, uy: 0, uz: 0, focal: 1, halfW: 0, halfH: 0, px: 0, py: 0, pz: 0 }; function UpdateCam(camInfo, screenW, screenH) { // 新 build:直接用相机自带的 world→clip 矩阵(g37_1),不依赖欧拉角约定 var M = camInfo.matrix; if (M && typeof M.length === 'number' && M.length >= 16) { Cam.M = M; Cam.hasM = true; } else { Cam.M = null; Cam.hasM = false; } var cp = Math.cos(camInfo.pitch), sp = Math.sin(camInfo.pitch); var cy = Math.cos(camInfo.yaw), sy = Math.sin(camInfo.yaw); Cam.fx = cp * (-sy); Cam.fy = cp * cy; Cam.fz = sp; // forward Cam.rx = cy; Cam.ry = sy; Cam.rz = 0; // right Cam.ux = Cam.ry * Cam.fz - Cam.rz * Cam.fy; // up = right × forward Cam.uy = Cam.rz * Cam.fx - Cam.rx * Cam.fz; Cam.uz = Cam.rx * Cam.fy - Cam.ry * Cam.fx; Cam.focal = screenH / (2 * Math.tan(camInfo.fov / 2)); Cam.halfW = screenW / 2; Cam.halfH = screenH / 2; Cam.screenW = screenW; Cam.screenH = screenH; Cam.px = camInfo.pos.x; Cam.py = camInfo.pos.y; Cam.pz = camInfo.pos.z; } // 顶点缓冲(全局复用,杜绝每帧每顶点对象分配) var VB_Wx = null, VB_Wy = null, VB_Wz = null; // 世界坐标(法线用) var VB_Sx = null, VB_Sy = null, VB_Ok = null; // 屏幕坐标 + 可投影标志 function EnsureVerts(n) { if (!VB_Wx || VB_Wx.length < n) { var cap = Math.max(n, 2048); VB_Wx = new Float32Array(cap); VB_Wy = new Float32Array(cap); VB_Wz = new Float32Array(cap); VB_Sx = new Float32Array(cap); VB_Sy = new Float32Array(cap); VB_Ok = new Uint8Array(cap); } } // 变换 + 投影一个 mesh 节点的全部顶点到复用缓冲,返回顶点数 function PrepareNodeVerts(meshData, worldMatrix, pos) { var positions = meshData.positions; var stride = meshData.stride || 3; var vc = Math.floor(positions.length / stride); EnsureVerts(vc); var m0 = worldMatrix[0], m1 = worldMatrix[1], m2 = worldMatrix[2], m4 = worldMatrix[4], m5 = worldMatrix[5], m6 = worldMatrix[6], m8 = worldMatrix[8], m9 = worldMatrix[9], m10 = worldMatrix[10], m12 = worldMatrix[12], m13 = worldMatrix[13], m14 = worldMatrix[14]; var px = pos.x, py = pos.y, pz = pos.z; // 新 build:矩阵投影(world→clip),完全跟随引擎自己的相机矩阵 if (Cam.hasM) { var M = Cam.M; var mm0 = M[0], mm1 = M[1], mm3 = M[3], mm4 = M[4], mm5 = M[5], mm7 = M[7], mm8 = M[8], mm9 = M[9], mm11 = M[11], mm12 = M[12], mm13 = M[13], mm15 = M[15]; var sw = Cam.screenW, sh = Cam.screenH; for (var v2 = 0; v2 < vc; v2++) { var b2x = positions[v2 * stride], b2y = positions[v2 * stride + 1], b2z = positions[v2 * stride + 2]; var w2x = m0 * b2x + m4 * b2y + m8 * b2z + m12; var w2y = m1 * b2x + m5 * b2y + m9 * b2z + m13; var w2z = m2 * b2x + m6 * b2y + m10 * b2z + m14; VB_Wx[v2] = w2x; VB_Wy[v2] = w2y; VB_Wz[v2] = w2z; var dd2x = w2x - px, dd2y = w2y - py, dd2z = w2z - pz; if (dd2x * dd2x + dd2y * dd2y + dd2z * dd2z > 6250000) { VB_Ok[v2] = 0; continue; } var ccx = mm0 * w2x + mm4 * w2y + mm8 * w2z + mm12; var ccy = mm1 * w2x + mm5 * w2y + mm9 * w2z + mm13; var ccw = mm3 * w2x + mm7 * w2y + mm11 * w2z + mm15; if (!(ccw > 0.0001)) { VB_Ok[v2] = 0; continue; } VB_Sx[v2] = (ccx / ccw * 0.5 + 0.5) * sw; VB_Sy[v2] = (1 - (ccy / ccw * 0.5 + 0.5)) * sh; VB_Ok[v2] = 1; } return vc; } var cfx = Cam.fx, cfy = Cam.fy, cfz = Cam.fz; var crx = Cam.rx, cry = Cam.ry, crz = Cam.rz; var cux = Cam.ux, cuy = Cam.uy, cuz = Cam.uz; var focal = Cam.focal, halfW = Cam.halfW, halfH = Cam.halfH; var cpx = Cam.px, cpy = Cam.py, cpz = Cam.pz; for (var v = 0; v < vc; v++) { var bx = positions[v * stride], by = positions[v * stride + 1], bz = positions[v * stride + 2]; var wx = m0 * bx + m4 * by + m8 * bz + m12; var wy = m1 * bx + m5 * by + m9 * bz + m13; var wz = m2 * bx + m6 * by + m10 * bz + m14; VB_Wx[v] = wx; VB_Wy[v] = wy; VB_Wz[v] = wz; var ddx = wx - px, ddy = wy - py, ddz = wz - pz; if (ddx * ddx + ddy * ddy + ddz * ddz > 6250000) { VB_Ok[v] = 0; continue; } var relX = wx - cpx, relY = wy - cpy, relZ = wz - cpz; var camZ = relX * cfx + relY * cfy + relZ * cfz; if (camZ <= 0.1) { VB_Ok[v] = 0; continue; } var camX = relX * crx + relY * cry + relZ * crz; var camY = relX * cux + relY * cuy + relZ * cuz; VB_Sx[v] = halfW + (camX / camZ) * focal; VB_Sy[v] = halfH - (camY / camZ) * focal; VB_Ok[v] = 1; } return vc; } // ===== 皮肤组件(新旧差异修正) ===== // 新版(main.7cfa4058): // skin 组件(类 eUt)构造后由 j16r 把皮肤对象存到 **组件自己的 g159_1**, // 皮肤对象.g17r_1 = 车身部件表;每个部件 part.j17r_1 是**单个 BP 节点**(旧版是节点列表)。 // 炮塔根节点 = skinComp.a17q_1(weaponSkin 组件 LUt.u11p() = e13a().a17q_1,e13a 取的就是 skin 组件)。 // 旧写法(w14w_1 / n11c_1 / m17a_1)在新版已被无关类占用(w14w_1=SetReverseBackTurn、m17a_1=数值参数),不再可用。 var SkinCompCache = new WeakMap(); function GetSkinComponent(entity) { if (!entity) return null; var Cached = SkinCompCache.get(entity); if (Cached) return Cached; var comp = FindComponentByField(entity, 'g159_1'); if (!comp) comp = FindComponentByField(entity, 'a17q_1'); // 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表) if (!comp) { try { entity.qz1_1.s2a_1.forEach(function (VL) { if (comp) return; var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; if (!C) return; for (var K in C) { var V = C[K]; if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; } } }); } catch (e) {} } if (comp) SkinCompCache.set(entity, comp); return comp; } // 取车身皮肤对象(含 g17r_1 部件表);stats 用于诊断来源 function GetHullSkin(tank, stats) { var comp = GetSkinComponent(tank); if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; } if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; } if (stats) stats.hullSource = '有组件但无皮肤'; return null; } // 炮塔 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); if (comp && comp.a17q_1) return comp.a17q_1; return null; } // 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v) function ForEachInList(list, fn) { if (!list) return; try { if (typeof list.e1 === 'function' && typeof list.g1 === 'function') { var n = list.e1(); for (var i = 0; i < n; i++) fn(list.g1(i)); return; } if (typeof list.t === 'function') { for (var it = list.t(); it.u();) fn(it.v()); } } catch (e) {} } // ===== 从GPU读取顶点位置数据(新版结构) ===== // 新旧差异(main.7cfa4058): // Tp(类 sm): x4w_1 = 顶点组列表,每组 { h3x_1: 顶点缓冲资源 J$, i3x_1: 属性布局 } // J$: v3x_1 = GPU 缓冲包装(kC,d75_1 = 原生 WebGLBuffer),w3x_1 = 顶点数 // 设备:V$.s3h_1 = GL 包装器 $C,$C.f75_1 = 原生 WebGL2RenderingContext // ⚠ 旧字段 d3x_1 / s3h_1.b74_1 / v3x_1.z73_1 在新版分别是"渲染参数 / 雾色 uniform / uniform 槽位",全错 function GetGlFromResource(Res) { if (!Res) return null; var Dev = Res.s3h_1; return (Dev && Dev.f75_1) ? Dev.f75_1 : null; } // 把引擎的 CPU 端缓冲区包装转成 TypedArray // J$.x3x_1 / Y$.p3x_1 = 未上传时的原始数据(上传后置 null) // 包装接口:v2u()=底层视图,g2t()=元素偏移,t2t()=元素个数 function ToTypedArray(Src, Kind) { if (!Src) return null; try { if (typeof Src.v2u === 'function') { var V = Src.v2u(); var Start = typeof Src.g2t === 'function' ? Src.g2t() : 0; var Count = typeof Src.t2t === 'function' ? Src.t2t() : (V && V.length); if (ArrayBuffer.isView(V) && Count > 0) { var ByteOff = V.byteOffset + Start * V.BYTES_PER_ELEMENT; if (Kind === 'u32') return new Uint32Array(V.buffer, ByteOff, Count); if (Kind === 'u16') return new Uint16Array(V.buffer, ByteOff, Count); return new Float32Array(V.buffer, ByteOff, Count); } } if (ArrayBuffer.isView(Src)) { if (Kind === 'u16') return (Src instanceof Uint16Array) ? Src : new Uint16Array(Src.buffer, Src.byteOffset, Src.byteLength >> 1); if (Kind === 'u32') return (Src instanceof Uint32Array) ? Src : new Uint32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2); return (Src instanceof Float32Array) ? Src : new Float32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2); } if (typeof Src.length === 'number' && Src.length > 0) { return (Kind === 'u16') ? Uint16Array.from(Src) : (Kind === 'u32' ? Uint32Array.from(Src) : Float32Array.from(Src)); } } catch (e) {} return null; } function ReadVertexPositionsFromTp(tp) { if (!tp || !tp.x4w_1 || typeof tp.x4w_1.e1 !== 'function' || tp.x4w_1.e1() === 0) return null; var group = tp.x4w_1.g1(0); if (!group) return null; var Buf = group.h3x_1; // J$ if (!Buf) return null; // ① 优先 CPU 端原始数据:未上传 GPU 的 mesh 也能读到(v3x_1.d75_1 此时为 null) var Cpu = ToTypedArray(Buf.x3x_1, 'f32'); if (Cpu && Cpu.length) return Cpu; // ② GPU 回读(引擎已上传并释放 CPU 数据时) var gl = GetGlFromResource(Buf); var webglBuf = Buf.v3x_1 && Buf.v3x_1.d75_1; if (!gl || !webglBuf) return null; gl.bindBuffer(gl.ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) return null; var f32 = new Float32Array(bufSize / 4); gl.getBufferSubData(gl.ARRAY_BUFFER, 0, f32); return f32; } // ===== 从GPU读取索引数据 ===== // y4w_1 = 索引列表,每项 { q4w_1: 索引缓冲资源 Y$ }(旧版是 il.u3p_1) function ReadIndicesFromTp(tp) { if (!tp.y4w_1 || typeof tp.y4w_1.e1 !== 'function') return null; var meshCount = tp.y4w_1.e1(); if (meshCount === 0) return null; var allIndices = []; for (var m = 0; m < meshCount; m++) { var il = tp.y4w_1.g1(m); if (!il) continue; var IdxBuf = il.q4w_1 || il.u3p_1; if (!IdxBuf) continue; // ① CPU 端索引数据(Y$.p3x_1),未上传时也能用 var Cpu = ToTypedArray(IdxBuf.p3x_1, 'u16'); if (Cpu && Cpu.length) { allIndices.push(Cpu); continue; } // ② GPU 回读 var gl = GetGlFromResource(IdxBuf); var webglBuf = IdxBuf.n3x_1 && IdxBuf.n3x_1.d75_1; if (!gl || !webglBuf) continue; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) continue; var indices = new Uint16Array(bufSize / 2); gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices); allIndices.push(indices); } return allIndices; } // ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) ===== var GpuDataCache = new Map(); var GpuCacheFrameCount = 0; 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); var positions = ReadVertexPositionsFromTp(tp); var indices = ReadIndicesFromTp(tp); if (positions && indices && indices.length > 0) { // 计算最大索引值,推断顶点 stride var maxIdx = 0; for (var i = 0; i < indices.length; i++) { var idxArr = indices[i]; for (var j = 0; j < idxArr.length; j++) { if (idxArr[j] > maxIdx) maxIdx = idxArr[j]; } } var stride = 0; // 优先用引擎记录的顶点数反推 stride(J$.w3x_1 = 顶点数) try { var G0 = tp.x4w_1.g1(0); var Vc = G0 && G0.h3x_1 && G0.h3x_1.w3x_1; if (Vc > 0 && positions.length % Vc === 0) stride = Math.round(positions.length / Vc); } catch (e) {} if (!stride || stride < 3 || stride > 20) { stride = 3; var vc3 = Math.floor(positions.length / 3); if (maxIdx >= vc3) { // stride=3 顶点数不够,尝试常见 interleaved 布局 var candidates = [8, 11, 5, 7, 14]; for (var c = 0; c < candidates.length; c++) { var vc = Math.floor(positions.length / candidates[c]); if (maxIdx < vc) { stride = candidates[c]; break; } } } } var data = { positions: positions, indices: indices, stride: stride }; GpuDataCache.set(tp, data); if (!StrideDiagPrinted.has(tp)) { StrideDiagPrinted.add(tp); console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride)); } 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; } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== // Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次 function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) { if (!bpNode || depth > 12) return; if (Seen) { if (Seen.has(bpNode)) return; Seen.add(bpNode); } if (stats) stats.seen++; var name = bpNode.g35_1 || ''; if (bpNode.v3m_1) { if (stats) stats.hasMesh++; var gpuData = GetGpuData(bpNode.v3m_1); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name, node: bpNode, tp: bpNode.v3m_1, positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') { try { var lodCount = bpNode.u4x_1.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { var lodLevel = bpNode.u4x_1.g1(lod); if (!lodLevel) continue; var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name + '_lod' + lod, node: bpNode, tp: lodTp, positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group }); } } } catch(e) {} } // 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点) // 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表 var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_1 : ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen); }); } } // 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 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'); } // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 function IsPlainObj(V) { if (!V || typeof V !== 'object') return false; if (Array.isArray(V) || ArrayBuffer.isView(V)) return false; if (typeof Map !== 'undefined' && V instanceof Map) return false; if (typeof Set !== 'undefined' && V instanceof Set) return false; return true; } function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); function Try(V) { if (!IsSceneNode(V) || Seen.has(V)) return; CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); } try { tank.qz1_1.s2a_1.forEach(function (VL) { var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; if (!IsPlainObj(comp)) return; for (var K in comp) { var V = comp[K]; if (!IsPlainObj(V)) 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 (!IsPlainObj(V2)) continue; if (IsSceneNode(V2)) { Try(V2); continue; } for (var K3 in V2) { var V3 = V2[K3]; if (IsPlainObj(V3)) Try(V3); } } } catch (e) {} } }); } catch (e) {} } // 结果级去重:同一 mesh 资源 + 同一节点名,只保留一份 // 注意:**不要把世界坐标写进键**——坦克移动时两套实例的平移会差一帧, // 键随位置变化会导致「这帧去重、下帧不去重」→ 模型闪烁。 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 Key = Id + ':' + N.name; if (Keys.has(Key)) continue; Keys.add(Key); Out.push(N); } return Out; } // ===== 提取坦克的mesh数据 ===== // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底) function GetTankMeshData(tank, stats) { var nodes = []; var Seen = new WeakSet(); var skinComp = GetSkinComponent(tank); var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; if (stats) { stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; } // ① 主路径:车身根节点(包含全部部件节点) if (skinComp && skinComp.h159_1) { try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } // ② 根节点采不到才退回部件表 if (nodes.length === 0 && skin) { try { ForEachInList(skin.g17r_1, function (part) { if (!part) return; var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen); }); } else { CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } }); } catch(e) {} } // ③ 仍然为空才做组件字段扫描(不加节流:节流会导致「这帧有模型、下帧只有点」的闪烁) if (nodes.length === 0) { 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) {} } return DedupNodes(nodes); } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 ===== // 现象:实体已移动,但原处仍留着线框。 // 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。 // 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点), // 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走: // 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。 // 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。 var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树) var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均) var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计) var NodeMatrixScratch = new Float32Array(16); function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) { // 世界矩阵:新版是 p35_1(zl 基类构造里 = hl(),s3k/t3k/u3k 读 p35_1[12..14]) // 旧版是 n35_1;新版 n35_1 已变成布尔标志 var M = bpNode.p35_1; if (!M || typeof M.length !== 'number' || M.length < 16) { var OldM = bpNode.n35_1; M = (OldM && typeof OldM.length === 'number' && OldM.length >= 16) ? OldM : null; } if (!M) return null; if (diagStats) diagStats.totalNodes++; var Hist = NodeMatrixHistory.get(bpNode); if (!Hist) { NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null }); } else { var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z; if (dx * dx + dy * dy + dz * dz > 0.01) { Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14]; Hist.still = 0; Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制 } else if (tankIsMoving) { Hist.still++; if (Hist.still >= FrozenFrames) { if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z }; for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k]; NodeMatrixScratch[12] = pos.x + Hist.off.x; NodeMatrixScratch[13] = pos.y + Hist.off.y; NodeMatrixScratch[14] = pos.z + Hist.off.z; if (diagStats) diagStats.draggedNodes++; return NodeMatrixScratch; // 冻结但跟随坦克 } } else { Hist.still = 0; } } // 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做 var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z; if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) { if (diagStats) diagStats.filteredNodes++; return null; } return M; } // ===== 线框模式:绘制单个节点的所有边 ===== // 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配) var _WfEdgeSet = new Set(); function WfAddEdge(ctx, a, b) { var lo = a < b ? a : b, hi = a < b ? b : a; var key = lo * 65536 + hi; if (_WfEdgeSet.has(key)) return; _WfEdgeSet.add(key); ctx.moveTo(VB_Sx[lo], VB_Sy[lo]); ctx.lineTo(VB_Sx[hi], VB_Sy[hi]); } function DrawWireframeNode(ctx, meshData, worldMatrix, pos) { var indicesArr = meshData.indices; var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); _WfEdgeSet.clear(); ctx.beginPath(); for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var idx = 0; idx < indices.length; idx += 3) { var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2]; if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue; WfAddEdge(ctx, i0, i1); WfAddEdge(ctx, i1, i2); WfAddEdge(ctx, i2, i0); } } ctx.stroke(); } // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { var p = ProjectPoint(pos, camInfo, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI); ctx.fill(); } // ===== 描边模式:屏幕空间光栅化轮廓提取 ===== // 算法(真正的"屏幕几何"方法): // 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码 // 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并 // 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素) // 4. 只画边界像素 = 屏幕空间外轮廓 var PixelMask = null; var PixelMaskW = 0; var PixelMaskH = 0; var MaskGen = 0; // 掩码用"代数"标记:本辆坦克填充的像素 = gen,无需在坦克之间清空整块缓冲 function GetPixelMask(w, h) { if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) { PixelMask = new Uint32Array(w * h); PixelMaskW = w; PixelMaskH = h; MaskGen = 0; } return PixelMask; } // 扫描线光栅化三角形(pixels 中写入 gen 表示"本辆坦克已填充") function RasterizeTriangle(pixels, w, h, gen, x0, y0, x1, y1, x2, y2) { // 按 y 排序三个顶点 if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } var minY = Math.max(0, Math.ceil(y0)); var maxY = Math.min(h - 1, Math.floor(y2)); if (minY > maxY) return; for (var y = minY; y <= maxY; y++) { var xa, xb; if (y < y1) { // 上半部分:y0 -> y1 和 y0 -> y2 xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } else { // 下半部分:y1 -> y2 和 y0 -> y2 xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } if (xa > xb) { var t = xa; xa = xb; xb = t; } var minXA = Math.max(0, Math.ceil(xa - 0.5)); var maxXA = Math.min(w - 1, Math.floor(xb + 0.5)); var rowOffset = y * w; for (var x = minXA; x <= maxXA; x++) { pixels[rowOffset + x] = gen; } } } function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, diagStats, color) { var pixels = GetPixelMask(screenW, screenH); var gen = ++MaskGen; if (gen === 0) { pixels.fill(0); gen = ++MaskGen; } // 代数回绕(理论值):整体清一次 // 记录坦克的屏幕边界框(用于后续边界检测优化) var minX = screenW, minY = screenH, maxX = 0, maxY = 0; for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats); if (!worldMatrix) continue; var indicesArr = meshData.indices; // 顶点变换+投影到复用缓冲(无对象分配) var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); for (var v = 0; v < vertexCount; v++) { if (!VB_Ok[v]) continue; var sx = VB_Sx[v], sy = VB_Sy[v]; if (sx < minX) minX = sx; if (sy < minY) minY = sy; if (sx > maxX) maxX = sx; if (sy > maxY) maxY = sy; } // 遍历三角形,光栅化面向相机的 for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var i = 0; i < indices.length; i += 3) { var a = indices[i], b = indices[i+1], c = indices[i+2]; if (!VB_Ok[a] || !VB_Ok[b] || !VB_Ok[c]) continue; // 计算法线判断是否面向相机(世界坐标,来自缓冲) var ax = VB_Wx[a], ay = VB_Wy[a], az = VB_Wz[a]; var bx = VB_Wx[b], by = VB_Wy[b], bz = VB_Wz[b]; var gx = VB_Wx[c], gy = VB_Wy[c], gz = VB_Wz[c]; var e1x = bx - ax, e1y = by - ay, e1z = bz - az; var e2x = gx - ax, e2y = gy - ay, e2z = gz - az; var nx = e1y * e2z - e1z * e2y; var ny = e1z * e2x - e1x * e2z; var nz = e1x * e2y - e1y * e2x; var cx = (ax + bx + gx) / 3; var cy = (ay + by + gy) / 3; var cz = (az + bz + gz) / 3; var dot = nx * (Cam.px - cx) + ny * (Cam.py - cy) + nz * (Cam.pz - cz); if (dot <= 0) continue; // 背向相机,跳过 // 光栅化三角形到像素掩码 RasterizeTriangle(pixels, screenW, screenH, gen, VB_Sx[a], VB_Sy[a], VB_Sx[b], VB_Sy[b], VB_Sx[c], VB_Sy[c]); } } } // 找出边界像素并绘制(只在坦克边界框内遍历;用单条路径 + 横向连续合并,替代逐像素 fillRect) var bx0 = Math.max(0, Math.floor(minX) - 1); var by0 = Math.max(0, Math.floor(minY) - 1); var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1); var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1); ctx.fillStyle = color; ctx.beginPath(); var outlineCount = 0; for (var y = by0; y <= by1; y++) { var rowOffset = y * screenW; var runStart = -1; for (var x = bx0; x <= bx1 + 1; x++) { var isBoundary = false; if (x <= bx1 && pixels[rowOffset + x] === gen) { // 检查4个邻居是否有未填充像素(== gen 即本辆坦克填充) if (x === 0 || pixels[rowOffset + x - 1] !== gen) isBoundary = true; else if (x === screenW - 1 || pixels[rowOffset + x + 1] !== gen) isBoundary = true; else if (y === 0 || pixels[rowOffset - screenW + x] !== gen) isBoundary = true; else if (y === screenH - 1 || pixels[rowOffset + screenW + x] !== gen) isBoundary = true; } if (isBoundary) { if (runStart < 0) runStart = x; outlineCount++; } else if (runStart >= 0) { ctx.rect(runStart, y, x - runStart + 2, 3); // 横向连续的边界像素合并为一条 runStart = -1; } } } ctx.fill(); if (diagStats) diagStats.outlineEdges += outlineCount; return outlineCount; } // ===== 创建全屏Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornet3DWireframeCanvas'; canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;'; document.body.appendChild(canvas); var ctx = canvas.getContext('2d'); function ResizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } ResizeCanvas(); window.addEventListener('resize', ResizeCanvas); // ===== 诊断统计 ===== var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, outlineEdges: 0 }; var LastDegradeLogFrame = -9999; // 退化诊断日志节流 // 退化诊断(节流输出):定位"存活坦克模型没画出来"的根因(皮肤/部件/GPU/炮塔逐级) function LogTankDiag(tankName, tankState, S, hullCount, turretCount, rendered) { if (esp.frameCount - LastDegradeLogFrame <= 120) return; LastDegradeLogFrame = esp.frameCount; console.warn('[透视][退化诊断] ' + tankName + ' state=' + tankState + ' | 皮肤=' + S.hullSource + ' 部件=' + S.hullParts + ' 车身[见/网格/GPU成功/失败]=' + S.hull.seen + '/' + S.hull.hasMesh + '/' + S.hull.gpuOk + '/' + S.hull.gpuFail + ' | 炮塔 组件=' + S.turretComp + ' 根节点=' + S.turretRoot + ' 炮塔[见/网格/GPU成功/失败]=' + S.turret.seen + '/' + S.turret.hasMesh + '/' + S.turret.gpuOk + '/' + S.turret.gpuFail + ' | 收集 车身=' + hullCount + ' 炮塔=' + turretCount + ' 绘制=' + rendered + ' | 本帧 总=' + DiagStats.totalNodes + ' 过滤=' + DiagStats.filteredNodes); } // ===== ESP状态 ===== var esp = { canvas: canvas, ctx: ctx, running: true, frameCount: 0, mode: Mode, setMode: function(m) { if (m === 'wireframe' || m === 'outline') { Mode = m; this.mode = m; try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {} console.log('[透视] 模式切换为: ' + m); } else { console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline'); } }, getMode: function() { return Mode; }, setLineWidth: function(w) { var n = parseFloat(w); if (!isNaN(n) && n >= 0.5 && n <= 8) { LineWidth = n; try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {} console.log('[透视] 线宽设置为: ' + n); } else { console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w); } }, getLineWidth: function() { return LineWidth; }, setHighlightMode: function(mode) { var valid = ['team','whitelist','blacklist']; if (valid.indexOf(mode) >= 0) { HighlightMode = mode; try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {} console.log('[透视] 渲染模式切换为: ' + mode); } else { console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', ')); } }, getHighlightMode: function() { return HighlightMode; }, setHighlightList: function(names) { HighlightList = Array.isArray(names) ? names.slice() : []; SaveHighlightList(); console.log('[透视] 高亮名单已更新:', HighlightList.length + '人'); }, addHighlight: function(name) { if (name && HighlightList.indexOf(name) < 0) { HighlightList.push(name); SaveHighlightList(); console.log('[透视] 已添加高亮:', name); } }, removeHighlight: function(name) { var idx = HighlightList.indexOf(name); if (idx >= 0) { HighlightList.splice(idx, 1); SaveHighlightList(); console.log('[透视] 已移除高亮:', name); } }, getHighlightList: function() { return HighlightList.slice(); }, clearHighlight: function() { HighlightList = []; SaveHighlightList(); console.log('[透视] 高亮名单已清空'); }, stop: function() { this.running = false; window.removeEventListener('resize', ResizeCanvas); if (canvas.parentNode) canvas.parentNode.removeChild(canvas); console.log('透视已停止'); } }; window.__hornet3DWireframe__ = esp; // ===== 金箱子标记 ===== // 金箱子是独立实体(不是坦克)。引擎侧标志: // · 补给实体 = entity.j10q_1(bonusData 组件,见 module_87072 的 ctt()) // · 类型枚举 mE(module_10032):GOLD_BOX = 6 // 做法:实体基类的组件创建方法 nyz()(module_61378「BattleEntity.createComponent」)上挂观察钩子, // 发现「带 j10q_1 且类型=6」的实体就登记,之后每帧按它自己的位置画金色标记。 // 钩子目标沿 tank 的原型链找 nyz 的宿主原型 —— 不需要任何导出键。 var GoldBoxes = []; var GoldPending = []; var EntityProtoHooked = false; var GOLD_BOX_TYPE = 6; function FindEnumOf(Obj) { for (var K in Obj) { try { var V = Obj[K]; if (V && typeof V === 'object' && typeof V.k3_1 === 'number' && typeof V.equals === 'function') return V; } catch (e) {} } return null; } // 引擎口径:有 bonusData 且类型是 GOLD_BOX function IsGoldBoxEntity(Entity) { if (!Entity || !Entity.j10q_1) return false; var Type = FindEnumOf(Entity.j10q_1); return !!(Type && Type.k3_1 === GOLD_BOX_TYPE); } function RegisterGoldBox(Entity) { for (var i = 0; i < GoldBoxes.length; i++) if (GoldBoxes[i] === Entity) return; GoldBoxes.push(Entity); console.log('[透视] 发现金箱子,已加入标记(当前 ' + GoldBoxes.length + ' 个)'); } function InstallEntityHook(Tank) { if (EntityProtoHooked || !Tank) return; var Proto = Tank; while (Proto && !Object.prototype.hasOwnProperty.call(Proto, 'nyz')) Proto = Object.getPrototypeOf(Proto); if (!Proto || typeof Proto.nyz !== 'function') return; var Orig = Proto.nyz; Proto.nyz = function () { var R = Orig.apply(this, arguments); try { if (IsGoldBoxEntity(this)) RegisterGoldBox(this); else if (this && this.j10q_1 === undefined && GoldPending.length < 300) { GoldPending.push({ e: this, f: esp.frameCount }); // 字段可能稍后才赋值,延后判定 } } catch (E) {} return R; }; EntityProtoHooked = true; console.log('[透视] 已挂补给实体观察钩子(金箱子)'); } // 金箱位置:优先物理组件(与坦克同款 rzf_1),否则退到 bonusData 里的节点世界矩阵/向量 function GetGoldBoxPosition(Entity) { var PhysPos = GetTankPosition(Entity); // 多级探测物理组件位置 if (PhysPos) return PhysPos; var Bonus = Entity.j10q_1; for (var K in Bonus) { try { var V = Bonus[K]; if (!V || typeof V !== 'object') continue; if (V.n35_1 && V.n35_1.length >= 15) { var M = V.n35_1; return { x: M[12], y: M[13], z: M[14] }; } if (typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { return { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; } } catch (e) {} } return null; } // 金色菱形 + 十字 + 「金箱」标签(穿透显示,不遮挡判断) function DrawGoldBoxMarker(Pos, ScreenW, ScreenH) { var X = Pos.x - Cam.px, Y = Pos.y - Cam.py, Z = Pos.z - Cam.pz; var CamZ = X * Cam.fx + Y * Cam.fy + Z * Cam.fz; if (CamZ <= 0.1) return false; var CamX = X * Cam.rx + Y * Cam.ry + Z * Cam.rz; var CamY = X * Cam.ux + Y * Cam.uy + Z * Cam.uz; var Sx = Cam.halfW + (CamX / CamZ) * Cam.focal; var Sy = Cam.halfH - (CamY / CamZ) * Cam.focal; if (Sx < -60 || Sy < -60 || Sx > ScreenW + 60 || Sy > ScreenH + 60) return false; var R = 12; ctx.save(); ctx.strokeStyle = '#ffd700'; ctx.fillStyle = 'rgba(255, 215, 0, 0.22)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(Sx, Sy - R); ctx.lineTo(Sx + R, Sy); ctx.lineTo(Sx, Sy + R); ctx.lineTo(Sx - R, Sy); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(Sx - R - 7, Sy); ctx.lineTo(Sx + R + 7, Sy); ctx.moveTo(Sx, Sy - R - 7); ctx.lineTo(Sx, Sy + R + 7); ctx.stroke(); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'center'; ctx.fillText('金箱', Sx, Sy - R - 9); ctx.restore(); return true; } // ===== 冻结节点"拖拽跟随"诊断(节流 2s)===== var LastFrozenLogAt = 0; function LogDragDiag(Stats) { if (!Stats.draggedNodes) return; var Now = Date.now(); if (Now - LastFrozenLogAt < 2000) return; LastFrozenLogAt = Now; console.log('[透视][冻结] 本帧拖拽跟随节点 ' + Stats.draggedNodes + '/' + Stats.totalNodes + '(引擎未更新这些节点矩阵,已按坦克位移平移,避免旧位置残留/模型丢失)'); } // ===== 渲染循环 ===== function Render() { if (!esp.running) return; esp.frameCount++; // 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; GpuDataCache.clear(); // 旧地图的 GPU 几何数据失效 TankPrevPos.clear(); // 位置历史(判断"在动")失效 GoldBoxes.length = 0; // 上一局的箱子实体全部作废 GoldPending.length = 0; GpuCacheFrameCount = 0; console.log('[透视] 检测到新战斗,已重置缓存'); } // 旧实例已失效、新实例尚未捕获:清屏并显示等待提示,避免继续用悬空引用 if (!window.__gameMode__) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255, 220, 120, 0.9)'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'left'; ctx.fillText('[' + Mode.toUpperCase() + '] 等待战局…(ght 未捕获)', 5, canvas.height - 5); requestAnimationFrame(Render); return; } // 定期清空 GPU 数据缓存 GpuCacheFrameCount++; if (GpuCacheFrameCount >= GpuCacheClearInterval) { GpuDataCache.clear(); GpuCacheFrameCount = 0; } DiagStats = { totalNodes: 0, filteredNodes: 0, draggedNodes: 0, renderedNodes: 0, outlineEdges: 0, goldBoxes: 0 }; try { var screenW = canvas.width; var screenH = canvas.height; ctx.clearRect(0, 0, screenW, screenH); var camInfo = GetCameraInfo(); if (!camInfo) { requestAnimationFrame(Render); return; } var playerTank = GetPlayerTank(); if (!playerTank) { requestAnimationFrame(Render); return; } // 相机位置为0时使用玩家位置 if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) { var pPos = GetTankPosition(playerTank); if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 }; } UpdateCam(camInfo, screenW, screenH); // 每帧只算一次相机基(需在相机位置兜底之后),供顶点变换/投影复用 var tankList = gm.m10b_1.j11y(); var count = tankList.e1(); var drawnCount = 0, botDrawn = 0; // 人机扫描(装备分数不会每帧变,节流到约每 0.3 秒一次) if (ScanBots && (Date.now() - LastBotScanAt > 300)) { LastBotScanAt = Date.now(); ScanBots(); } for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!EntityProtoHooked) InstallEntityHook(tank); // 借坦克实例找到实体基类原型,挂补给观察钩子 var pos = GetTankPosition(tank); // 多级探测物理组件位置(字段名每个构建会平移) if (!pos) continue; var tankState = GetTankState(tank); // 可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2)(引擎 s14j() 判定),0/1/4 才跳过 if (tankState !== 2 && tankState !== 3) continue; // 本帧该坦克是否在移动(供「残留节点」检测:坦克在动而节点矩阵不动 = 旧位置遗留) // 用位移的指数滑动平均,而不是「单帧位移 > 10 单位」——后者对慢速移动几乎永远为假, // 会让残留检测形同虚设(残留漏掉的根因就在这里) var tankId = tank.iz1_1; var prevPos = TankPrevPos.get(tankId); var tankMove = 0; if (prevPos) { var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z; tankMove = (prevPos.move || 0) * 0.85 + Math.sqrt(pdx * pdx + pdy * pdy + pdz * pdz); } var tankIsMoving = tankMove > 0.5; // 0.5 单位/帧 ≈ 0.3 m/s;静置坦克的抖动低于此值 TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z, move: tankMove }); // 距离检查(50000 = 500米) var dx = pos.x - camInfo.pos.x; var dy = pos.y - camInfo.pos.y; var dz = pos.z - camInfo.pos.z; var distance = Math.sqrt(dx*dx + dy*dy + dz*dz); if (distance > 50000) continue; var friendly = IsFriendly(tank, playerTank); var tankName = GetTankName(tank) || ('ID:' + tank.iz1_1); // 根据 HighlightMode 判断是否渲染该坦克 if (!ShouldRender(tankName)) continue; // 颜色按队伍:友方=蓝,敌方=红 var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)'; // 3D模型渲染(仅在mesh数据可用时) var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 } }; var meshNodes = GetTankMeshData(tank, MeshStats); if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 drawnCount++; continue; } // 统计各分组节点数(诊断用) var hullCount = 0, turretCount = 0; for (var gi = 0; gi < meshNodes.length; gi++) { if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++; } // 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点 // (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉) if (hullCount === 0 && turretCount === 0) { LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); drawnCount++; continue; } ctx.strokeStyle = color; ctx.lineWidth = LineWidth; var renderedThisTank = 0; if (Mode === 'wireframe') { for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats); if (!worldMatrix) continue; DrawWireframeNode(ctx, meshData, worldMatrix, pos); DiagStats.renderedNodes++; renderedThisTank++; } } else { renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color); } // 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位 if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) { LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank); } // 一个节点都没画出来:画兜底标记,避免坦克完全不可见 if (renderedThisTank === 0) { DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); } // 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000) if (typeof IsBot === 'function' && IsBot(tank)) { var botPt = ProjectPoint({ x: pos.x, y: pos.y, z: pos.z + 1300 }, camInfo, screenW, screenH); if (botPt) { ctx.save(); ctx.font = 'bold 13px Consolas,monospace'; ctx.textAlign = 'center'; ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.strokeText('人机', botPt.x, botPt.y); // 先描黑边,保证任何背景下都看得清 ctx.fillStyle = '#ffd24a'; ctx.fillText('人机', botPt.x, botPt.y); ctx.restore(); botDrawn++; } } drawnCount++; } // ===== 金箱子:清理失效项 → 评估候选 → 画金色标记 ===== var goldDrawn = 0; for (var gci = GoldBoxes.length - 1; gci >= 0; gci--) { if (!IsGoldBoxEntity(GoldBoxes[gci])) GoldBoxes.splice(gci, 1); // 被拾取/销毁 } for (var gpi = GoldPending.length - 1; gpi >= 0; gpi--) { var PendingIt = GoldPending[gpi]; if (esp.frameCount - PendingIt.f > 90) { GoldPending.splice(gpi, 1); continue; } if (IsGoldBoxEntity(PendingIt.e)) { RegisterGoldBox(PendingIt.e); GoldPending.splice(gpi, 1); } } for (var gbi = 0; gbi < GoldBoxes.length; gbi++) { var GoldPos = GetGoldBoxPosition(GoldBoxes[gbi]); if (GoldPos && DrawGoldBoxMarker(GoldPos, screenW, screenH)) goldDrawn++; } DiagStats.goldBoxes = goldDrawn; // 屏幕信息 ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks'; if (Mode === 'outline') { infoText += ' | Edges: ' + DiagStats.outlineEdges; } else { infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes; } infoText += ' | Frame: ' + esp.frameCount; if (DiagStats.draggedNodes > 0) infoText += ' | 拖拽:' + DiagStats.draggedNodes; if (DiagStats.goldBoxes > 0) infoText += ' | 金箱:' + DiagStats.goldBoxes; ctx.fillText(infoText, 5, screenH - 5); LogDragDiag(DiagStats); } catch(e) { console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : ''); } requestAnimationFrame(Render); } // 启动渲染 requestAnimationFrame(Render); console.log('3D透视已启动(双模式: wireframe | outline)'); console.log('当前模式: ' + Mode); console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式'); console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式'); console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)'); console.log('提示: window.__hornet3DWireframe__.stop() 停止透视'); } var Tool = { Start: function() { if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return; _start(); }, Stop: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop(); }, Status: function() { return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止'; }, GetRunning: function() { return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running); }, SetMode: function(m) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m); }, GetMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null; }, SetLineWidth: function(w) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w); }, GetLineWidth: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2; }, SetHighlightMode: function(mode) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode); }, GetHighlightMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team'; }, SetHighlightList: function(names) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names); }, AddHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name); }, RemoveHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name); }, GetHighlightList: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : []; }, ClearHighlight: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight(); } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Wireframe = Tool; console.log('[3DTank] 3D透视模块已加载'); })();