// ===== 3DTank3D透视 - 线框/描边双模式 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在全屏叠加层上绘制敌方坦克的3D模型 // 模式: // wireframe - 完整线框(绘制所有边) // outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点) // 切换:window.__hornet3DWireframe__.setMode('outline') (function() { function _start() { console.log('===== 3DTank3D透视启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } // ===== 清理旧透视 ===== if (window.__hornet3DWireframe__) { window.__hornet3DWireframe__.stop(); console.log('已停止旧透视'); } if (window.__hornet3DOutline__) { window.__hornet3DOutline__.stop(); console.log('已停止旧描边透视'); } var gm = window.__gameMode__; // ===== 当前模式 ===== 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; var GetTankHealth = lib.GetTankHealth; 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 TransformVertex = lib.TransformVertex; // ===== 获取 KMt / TDt 渲染组件 ===== function GetKMtComponent(entity) { var comp = FindComponentByField(entity, 'u14l_1'); if (comp) return comp; comp = FindComponentByField(entity, 't14l_1'); if (comp) return comp; comp = FindComponentByField(entity, 'p14l_1'); return comp; } function GetTDtComponent(entity) { var comp = FindComponentByField(entity, 'n11c_1'); if (comp) return comp; comp = FindComponentByField(entity, 'q112_1'); return comp; } // ===== 从GPU读取顶点位置数据(缓冲区[0]) ===== function ReadVertexPositionsFromTp(tp) { if (!tp || !tp.t4w_1) return null; var tl = tp.t4w_1.g1(0); if (!tl || !tl.d3x_1) return null; var vq = tl.d3x_1; var bs = vq.s3h_1; if (!bs || !bs.b74_1) return null; var gl = bs.b74_1; var webglBuf = vq.r3x_1 && vq.r3x_1.z73_1; if (!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读取索引数据 ===== 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 || !il.q3p_1) continue; var fq = il.q3p_1; var bs = fq.s3h_1; if (!bs || !bs.b74_1) continue; var gl = bs.b74_1; var webglBuf = fq.j3x_1 && fq.j3x_1.z73_1; if (!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 = 600; var StrideDiagPrinted = 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 = 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; } return null; } // ===== 幽灵节点检测(三重检测:z34_1 + 世界矩阵 + 位置) ===== var NodeMatrixHistory = new WeakMap(); var TankPrevPos = new Map(); var GhostThreshold = 30; var GhostCooldown = 120; function IsNodeGhost(bpNode, tankIsMoving) { if (!bpNode || !bpNode.n35_1) return false; // 方案1:z34_1 字段检测(主方案) var z34Val = bpNode.g35_1; if (z34Val !== undefined && z34Val < 0.9) return true; // 方案2:世界矩阵更新检测(后备方案) var curMatrix = bpNode.n35_1; var hist = NodeMatrixHistory.get(bpNode); if (!hist) { hist = { m: new Array(16), unchanged: 0, cooldown: 0 }; for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i]; NodeMatrixHistory.set(bpNode, hist); return false; } var same = true; for (var i = 0; i < 16; i++) { if (Math.abs(hist.m[i] - curMatrix[i]) > 0.01) { same = false; break; } } if (same) hist.unchanged++; else { hist.unchanged = 0; for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i]; } if (hist.cooldown > 0) { hist.cooldown--; return true; } if (tankIsMoving && hist.unchanged > GhostThreshold) { hist.cooldown = GhostCooldown; return true; } return false; } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== function CollectMeshNodes(bpNode, nodes, depth, group) { if (!bpNode || depth > 5) return; var name = bpNode.e35_1 || ''; if (bpNode.t3m_1) { var gpuData = GetGpuData(bpNode.t3m_1); if (gpuData) { nodes.push({ name: name, node: bpNode, tp: bpNode.t3m_1, positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } } if (bpNode.q4x_1 && typeof bpNode.q4x_1.e1 === 'function') { try { var lodCount = bpNode.q4x_1.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { var lodLevel = bpNode.q4x_1.g1(lod); if (!lodLevel) continue; var lodTp = lodLevel.t3m_1 || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { nodes.push({ name: name + '_lod' + lod, node: bpNode, tp: lodTp, positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group }); } } } catch(e) {} } if (bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') { var childCount = bpNode.p35_1.e1(); for (var c = 0; c < childCount; c++) { try { var child = bpNode.p35_1.g1(c); if (child) CollectMeshNodes(child, nodes, depth + 1, group); } catch(e) {} } } } // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank) { var nodes = []; var kmt = GetKMtComponent(tank); if (kmt && kmt.t14l_1 && kmt.t14l_1.s17b_1) { var skin = kmt.t14l_1; try { var partCount = skin.s17b_1.e1(); for (var p = 0; p < partCount; p++) { var part = skin.s17b_1.g1(p); if (!part || !part.v17b_1) continue; var nodeCount = part.v17b_1.e1(); for (var n = 0; n < nodeCount; n++) { var bpNode = part.v17b_1.g1(n); if (!bpNode) continue; CollectMeshNodes(bpNode, nodes, 0, 'hull'); } } } catch(e) {} } var tdt = GetTDtComponent(tank); if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) { try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret'); } catch(e) {} } return nodes; } // ===== 检查节点是否有效(幽灵检测 + 位置检查) ===== function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) { var worldMatrix = bpNode.n35_1; if (!worldMatrix) return null; if (diagStats) diagStats.totalNodes++; // 幽灵检测1:z34_1 字段 var z34Val = bpNode.g35_1; if (z34Val !== undefined && z34Val < 0.9) { if (diagStats) diagStats.ghostNodes++; return null; } // 幽灵检测2:世界矩阵更新 if (IsNodeGhost(bpNode, tankIsMoving)) { if (diagStats) diagStats.ghostNodes++; return null; } // 位置检查 var nodeDx = worldMatrix[12] - pos.x; var nodeDy = worldMatrix[13] - pos.y; var nodeDz = worldMatrix[14] - pos.z; if (nodeDx*nodeDx + nodeDy*nodeDy + nodeDz*nodeDz > 4000000) { if (diagStats) diagStats.filteredNodes++; return null; } return worldMatrix; } // ===== 线框模式:绘制单个节点的所有边 ===== function DrawWireframeNode(ctx, meshData, worldMatrix, pos, camInfo, screenW, screenH) { var positions = meshData.positions; var indicesArr = meshData.indices; var stride = meshData.stride || 3; var vertexCount = Math.floor(positions.length / stride); var projected = new Array(vertexCount); for (var v = 0; v < vertexCount; v++) { var wp = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]); // 顶点级合理性检查 var ddx = wp.x - pos.x, ddy = wp.y - pos.y, ddz = wp.z - pos.z; if (ddx*ddx + ddy*ddy + ddz*ddz > 6250000) { projected[v] = null; continue; } projected[v] = Project3D(wp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); } var drawnEdges = new Set(); 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 (!projected[i0] || !projected[i1] || !projected[i2]) continue; var edges = [ [Math.min(i0, i1), Math.max(i0, i1)], [Math.min(i1, i2), Math.max(i1, i2)], [Math.min(i2, i0), Math.max(i2, i0)] ]; for (var e = 0; e < 3; e++) { var key = edges[e][0] + '_' + edges[e][1]; if (drawnEdges.has(key)) continue; drawnEdges.add(key); ctx.moveTo(projected[edges[e][0]].x, projected[edges[e][0]].y); ctx.lineTo(projected[edges[e][1]].x, projected[edges[e][1]].y); } } } ctx.stroke(); } // ===== 描边模式:屏幕空间光栅化轮廓提取 ===== // 算法(真正的"屏幕几何"方法): // 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码 // 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并 // 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素) // 4. 只画边界像素 = 屏幕空间外轮廓 var PixelMask = null; var PixelMaskW = 0; var PixelMaskH = 0; function GetPixelMask(w, h) { if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) { PixelMask = new Uint8Array(w * h); PixelMaskW = w; PixelMaskH = h; } else { PixelMask.fill(0); } return PixelMask; } // 扫描线光栅化三角形 function RasterizeTriangle(pixels, w, h, 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] = 1; } } } function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, diagStats, color) { var pixels = GetPixelMask(screenW, screenH); // 记录坦克的屏幕边界框(用于后续边界检测优化) 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 positions = meshData.positions; var indicesArr = meshData.indices; var stride = meshData.stride || 3; // 变换顶点到世界空间并投影到屏幕 var vertexCount = Math.floor(positions.length / stride); var worldVerts = new Array(vertexCount); var screenVerts = new Array(vertexCount); for (var v = 0; v < vertexCount; v++) { worldVerts[v] = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]); screenVerts[v] = Project3D(worldVerts[v], camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (screenVerts[v]) { if (screenVerts[v].x < minX) minX = screenVerts[v].x; if (screenVerts[v].y < minY) minY = screenVerts[v].y; if (screenVerts[v].x > maxX) maxX = screenVerts[v].x; if (screenVerts[v].y > maxY) maxY = screenVerts[v].y; } } // 遍历三角形,光栅化面向相机的 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]; var va = worldVerts[a], vb = worldVerts[b], vc = worldVerts[c]; var sa = screenVerts[a], sb = screenVerts[b], sc = screenVerts[c]; if (!sa || !sb || !sc) continue; // 计算法线判断是否面向相机 var e1x = vb.x - va.x, e1y = vb.y - va.y, e1z = vb.z - va.z; var e2x = vc.x - va.x, e2y = vc.y - va.y, e2z = vc.z - va.z; var nx = e1y * e2z - e1z * e2y; var ny = e1z * e2x - e1x * e2z; var nz = e1x * e2y - e1y * e2x; var cx = (va.x + vb.x + vc.x) / 3; var cy = (va.y + vb.y + vc.y) / 3; var cz = (va.z + vb.z + vc.z) / 3; var viewX = camInfo.pos.x - cx; var viewY = camInfo.pos.y - cy; var viewZ = camInfo.pos.z - cz; var dot = nx * viewX + ny * viewY + nz * viewZ; if (dot <= 0) continue; // 背向相机,跳过 // 光栅化三角形到像素掩码 RasterizeTriangle(pixels, screenW, screenH, sa.x, sa.y, sb.x, sb.y, sc.x, sc.y); } } } // 找出边界像素并绘制(只在坦克边界框内遍历) 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; var outlineCount = 0; for (var y = by0; y <= by1; y++) { var rowOffset = y * screenW; for (var x = bx0; x <= bx1; x++) { if (pixels[rowOffset + x] === 0) continue; // 检查4个邻居是否有未填充像素 var isBoundary = false; if (x === 0 || pixels[rowOffset + x - 1] === 0) isBoundary = true; else if (x === screenW - 1 || pixels[rowOffset + x + 1] === 0) isBoundary = true; else if (y === 0 || pixels[rowOffset - screenW + x] === 0) isBoundary = true; else if (y === screenH - 1 || pixels[rowOffset + screenW + x] === 0) isBoundary = true; if (isBoundary) { ctx.fillRect(x, y, 3, 3); outlineCount++; } } } // 清除像素掩码(供下一个坦克使用) for (var y2 = by0; y2 <= by1; y2++) { var rowOff = y2 * screenW; for (var x2 = bx0; x2 <= bx1; x2++) { pixels[rowOff + x2] = 0; } } 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, ghostNodes: 0, outlineEdges: 0 }; // ===== 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; // ===== 渲染循环 ===== function Render() { if (!esp.running) return; esp.frameCount++; // 定期清空 GPU 数据缓存 GpuCacheFrameCount++; if (GpuCacheFrameCount >= GpuCacheClearInterval) { GpuDataCache.clear(); GpuCacheFrameCount = 0; } DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, ghostNodes: 0, outlineEdges: 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 }; } var tankList = gm.q100_1.r11l(); var count = tankList.e1(); var drawnCount = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; var physicsComp = FindComponentByField(tank, 'gzh_1'); if (!physicsComp || !physicsComp.gzh_1) continue; var tankState = GetTankState(tank); if (tankState !== 3) continue; var pos = { x: physicsComp.gzh_1.f20_1, y: physicsComp.gzh_1.g20_1, z: physicsComp.gzh_1.h20_1 }; // 坦克移动检测 var tankId = tank.hyt_1; var prevPos = TankPrevPos.get(tankId); var tankIsMoving = false; if (prevPos) { var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z; tankIsMoving = (pdx*pdx + pdy*pdy + pdz*pdz) > 100; } TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z }); // 距离检查(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.hyt_1); // 根据 HighlightMode 判断是否渲染该坦克 if (!ShouldRender(tankName)) continue; // 颜色按队伍:友方=蓝,敌方=红 var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)'; var tankColor = friendly ? 'rgba(68, 136, 255, 0.9)' : 'rgba(255, 68, 68, 0.9)'; // 先画中心点、距离和血量(独立于3D模型渲染) var centerProj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (centerProj) { // 血量信息 var health = GetTankHealth(tank); var barW = 60, barH = 6; var barX = centerProj.x - barW / 2; var barY = centerProj.y - barH - 4; if (health) { // 背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillRect(barX - 1, barY - 1, barW + 2, barH + 2); // 血量百分比颜色:绿>60%, 黄>30%, 红 var hpColor = health.percent > 0.6 ? '#44ff44' : health.percent > 0.3 ? '#ffaa00' : '#ff4444'; ctx.fillStyle = hpColor; ctx.fillRect(barX, barY, barW * health.percent, barH); // 边框 ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.lineWidth = 1; ctx.strokeRect(barX, barY, barW, barH); // 名称(血量条上方) ctx.fillStyle = tankColor; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(tankName, centerProj.x, barY - 14); // 距离(血量条上方第二行) ctx.font = '10px Arial'; ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, barY - 3); // 血量数值(血量条下方) ctx.fillStyle = hpColor; ctx.font = '10px Arial'; ctx.fillText(Math.round(health.current) + '/' + Math.round(health.max), centerProj.x, barY + barH + 10); } else { ctx.fillStyle = tankColor; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(tankName, centerProj.x, centerProj.y - 18); ctx.font = '10px Arial'; ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, centerProj.y - 6); } // 中心点 ctx.fillStyle = tankColor; ctx.beginPath(); ctx.arc(centerProj.x, centerProj.y, 2, 0, 2 * Math.PI); ctx.fill(); } // 3D模型渲染(仅在mesh数据可用时) var meshNodes = GetTankMeshData(tank); if (!meshNodes || meshNodes.length === 0) { drawnCount++; continue; } // 检查是否有炮塔 var hasTurret = false; for (var ti = 0; ti < meshNodes.length; ti++) { if (meshNodes[ti].group === 'turret') { hasTurret = true; break; } } if (!hasTurret) { drawnCount++; continue; } ctx.strokeStyle = color; ctx.lineWidth = LineWidth; 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, camInfo, screenW, screenH); DiagStats.renderedNodes++; } } else { DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, DiagStats, color); } drawnCount++; } // 玩家血量显示(左上角) var playerHealth = GetTankHealth(playerTank); if (playerHealth) { var pBarW = 200, pBarH = 12; var pBarX = 10, pBarY = 10; // 背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillRect(pBarX - 1, pBarY - 1, pBarW + 2, pBarH + 2); // 血量颜色 var pHpColor = playerHealth.percent > 0.6 ? '#44ff44' : playerHealth.percent > 0.3 ? '#ffaa00' : '#ff4444'; ctx.fillStyle = pHpColor; ctx.fillRect(pBarX, pBarY, pBarW * playerHealth.percent, pBarH); // 边框 ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.lineWidth = 1; ctx.strokeRect(pBarX, pBarY, pBarW, pBarH); // 文字 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('HP: ' + Math.round(playerHealth.current) + ' / ' + Math.round(playerHealth.max) + ' (' + Math.round(playerHealth.percent * 100) + '%)', pBarX + pBarW / 2, pBarY + pBarH - 1); } // 屏幕信息 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 += ' | Ghost: ' + DiagStats.ghostNodes + ' | Frame: ' + esp.frameCount; ctx.fillText(infoText, 5, screenH - 5); } 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透视模块已加载'); })();