旧 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 重新生成。
923 lines
34 KiB
JavaScript
923 lines
34 KiB
JavaScript
// ===== 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透视模块已加载');
|
||
})();
|