Files
TankIC/tools/hornet_3d_wireframe.js
T
TermiNexus 90c377ad79 feat(3dtank): 本地镜像与 hornet 工具链迁到新版 bundle main.6469b966.js
旧 main.b7114b43.js 已被新构建替换(10,011,039 字节 / 1335 模块),本地镜像整体重切到新版并清掉旧产物;hornet 工具链按 tools/map_symbols.js 生成的新旧符号映射改名后重新打包(3dtank_bundle.js 与 unpacked/hornet_bundle.js 同步)。tools/symbol_map.json(8.9MB 映射产物)与切割解包目录不进库,可用 map_symbols.js / split.js 重新生成。
2026-09-17 19:59:40 +08:00

923 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ===== 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透视模块已加载');
})();