- 双向校验误判:q100_1 / r11l / jyw / t101_1 在新版同名但含义不同(普通数值 / 别的方法),被当作未变而跳过 - 强制替换:q100_1 到 m10b_1(坦克管理)、r11l 到 j11y(坦克列表)、jyw 到 lz4(取实体)、t101_1 到 p10c_1 - 覆盖 hook_gamemode.js(文件名不匹配 hornet_ 被漏掉) - 物理组件字段 gzh_1 改为 rzf_1(新版 gzh_1 已是 TerminatorCC 字段,非物理) - 移除已不存在的 xyu(GameMode),改用 gm.lz4()(等价:取 GameMode 实体) - 重新生成 hornet_bundle.js(10 模块 / 249229 字节)并同步 unpacked
681 lines
26 KiB
JavaScript
681 lines
26 KiB
JavaScript
// ===== 3DTank后视镜 - 原生渲染方案 =====
|
||
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
|
||
// 功能:复用游戏原生渲染器(Ff.y3s)+ FBO 重定向,在小 canvas 中渲染玩家背后的 3D 场景
|
||
// 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用后视相机参数调用游戏渲染器,readback 显示
|
||
|
||
(function() {
|
||
function _start() {
|
||
console.log('===== 3DTank后视镜(原生渲染版)启动 =====');
|
||
|
||
if (!window.__gameMode__) {
|
||
console.error('请先运行 hook_gamemode.js');
|
||
return;
|
||
}
|
||
|
||
if (window.__hornetRearview__) {
|
||
window.__hornetRearview__.Stop();
|
||
console.log('已停止旧后视镜');
|
||
}
|
||
|
||
var gm = window.__gameMode__;
|
||
|
||
// ===== 引用公共库(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() { return lib.GetCameraInfo(gm); }
|
||
|
||
// ===== 配置 =====
|
||
var Config = {
|
||
width: 400, height: 240, fov: 1.2,
|
||
maxRange: 15000, pitchOffset: -0.05,
|
||
cameraHeight: 250, near: 50, far: 30000,
|
||
refreshInterval: 0 // 每帧渲染(0=每帧,用 PBO 异步回读避免 GPU 停顿)
|
||
};
|
||
// 从 localStorage 恢复 FOV 和透明度
|
||
try {
|
||
var __savedFov = localStorage.getItem('hornet_rearview_fov');
|
||
if (__savedFov) {
|
||
var __fv = parseFloat(__savedFov);
|
||
if (!isNaN(__fv) && __fv >= 0.5 && __fv <= 2.5) Config.fov = __fv;
|
||
}
|
||
} catch(e) {}
|
||
var RearviewOpacity = 1.0;
|
||
try {
|
||
var __savedROp = localStorage.getItem('hornet_rearview_opacity');
|
||
if (__savedROp) {
|
||
var __rop = parseFloat(__savedROp);
|
||
if (!isNaN(__rop) && __rop >= 0.2 && __rop <= 1) RearviewOpacity = __rop;
|
||
}
|
||
} catch(e) {}
|
||
|
||
// ===== UI 层禁用(防止技能条等 UI 混入后视 FBO)=====
|
||
// ff.x38_1 是渲染层队列数组,索引对应层:
|
||
// 20=Hud, 21=GuiBack, 22=GuiMain, 23=GuiFront, 24=DebugGui
|
||
// 替换为空假对象,使 z3k 填充和渲染都跳过这些层
|
||
var uiLayerIndices = [20, 21, 22, 23, 24];
|
||
var fakeQueue = {
|
||
e1: function() { return 0; },
|
||
g1: function(i) { return null; },
|
||
w2a: function(x) {},
|
||
d3: function() {},
|
||
ii: function(x) {}
|
||
};
|
||
|
||
// ===== 获取游戏渲染对象 =====
|
||
var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null;
|
||
var initError = null;
|
||
|
||
try {
|
||
scene = gm.lz4().jz1_1;
|
||
console.log('[后视] 场景对象:', scene);
|
||
|
||
// hC 渲染器实例(jr.lye_1)
|
||
var renderer = scene.gyw_1;
|
||
console.log('[后视] 渲染器:', renderer);
|
||
|
||
// Ff 绘制器实例(hC.r74_1)
|
||
ff = renderer.d75_1;
|
||
console.log('[后视] Ff绘制器:', ff);
|
||
|
||
// 场景根节点(jr.kyf_1)
|
||
rootNode = scene.fyx_1;
|
||
console.log('[后视] 根节点:', rootNode);
|
||
|
||
// 主相机 Ha 实例(jr.nyf_1)
|
||
mainCamera = scene.iyx_1;
|
||
console.log('[后视] 主相机:', mainCamera);
|
||
|
||
// 游戏 gl context(Ff.p39_1.p73_1 = BS.p73_1)
|
||
gl = ff.w39_1.b74_1;
|
||
console.log('[后视] GL上下文:', gl);
|
||
|
||
if (!ff || !rootNode || !mainCamera || !gl) {
|
||
throw new Error('关键字段缺失');
|
||
}
|
||
|
||
// 探测相机字段
|
||
console.log('[后视] 相机u35_1:', mainCamera.u35_1);
|
||
console.log('[后视] 相机r35_1:', mainCamera.r35_1);
|
||
console.log('[后视] 相机b37_1:', mainCamera.b37_1);
|
||
|
||
if (!mainCamera.u35_1 || !mainCamera.r35_1) {
|
||
throw new Error('相机字段缺失');
|
||
}
|
||
|
||
} catch(e) {
|
||
initError = e;
|
||
console.error('[后视] 初始化失败:', e.message, e.stack);
|
||
}
|
||
|
||
// ===== 创建 UI canvas(显示后视画面 + UI 标记)=====
|
||
var uiCanvas = document.createElement('canvas');
|
||
uiCanvas.width = Config.width;
|
||
uiCanvas.height = Config.height;
|
||
uiCanvas.style.cssText = 'position:fixed;top:10px;right:10px;width:' + Config.width + 'px;height:' + Config.height + 'px;z-index:99999;border:2px solid rgba(120,200,255,0.6);border-radius:6px;pointer-events:auto;cursor:move;box-shadow:0 0 12px rgba(0,0,0,0.6);opacity:' + RearviewOpacity + ';';
|
||
document.body.appendChild(uiCanvas);
|
||
if (lib.MakeDraggable) lib.MakeDraggable(uiCanvas, 'rearview');
|
||
var uiCtx = uiCanvas.getContext('2d');
|
||
|
||
// ===== UI 绘制函数 =====
|
||
function DrawText(text, x, y, color, font, align, baseline) {
|
||
uiCtx.font = font || '12px monospace';
|
||
uiCtx.textAlign = align || 'left';
|
||
uiCtx.textBaseline = baseline || 'middle';
|
||
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
|
||
uiCtx.fillText(text, x, y);
|
||
}
|
||
|
||
function DrawTextOutlined(text, x, y, color, font, align, baseline) {
|
||
uiCtx.font = font || '12px monospace';
|
||
uiCtx.textAlign = align || 'left';
|
||
uiCtx.textBaseline = baseline || 'middle';
|
||
uiCtx.strokeStyle = 'rgba(0,0,0,0.85)';
|
||
uiCtx.lineWidth = 3;
|
||
uiCtx.strokeText(text, x, y);
|
||
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
|
||
uiCtx.fillText(text, x, y);
|
||
}
|
||
|
||
// ===== FBO 创建(后视镜渲染目标)=====
|
||
var rw = Config.width, rh = Config.height;
|
||
var rearFbo = null, rearTex = null, rearDepthRb = null;
|
||
var pixels = new Uint8Array(rw * rh * 4);
|
||
var imageData = uiCtx.createImageData(rw, rh);
|
||
|
||
// PBO 双缓冲异步回读(避免 readPixels 同步 GPU 停顿)
|
||
var pbos = [null, null];
|
||
var pboIndex = 0;
|
||
var pboReady = false; // 第一帧没有前帧数据,跳过回读
|
||
var pboSupported = false;
|
||
|
||
// Uint32 视图用于批量设置 alpha=255(修复树叶等半透明边缘)
|
||
var pixels32 = new Uint32Array(pixels.buffer);
|
||
|
||
if (gl && !initError) {
|
||
try {
|
||
rearFbo = gl.createFramebuffer();
|
||
rearTex = gl.createTexture();
|
||
rearDepthRb = gl.createRenderbuffer();
|
||
|
||
// 颜色纹理
|
||
gl.bindTexture(gl.TEXTURE_2D, rearTex);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rw, rh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||
|
||
// 深度缓冲
|
||
gl.bindRenderbuffer(gl.RENDERBUFFER, rearDepthRb);
|
||
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rw, rh);
|
||
|
||
// 组装 FBO
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, rearFbo);
|
||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rearTex, 0);
|
||
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rearDepthRb);
|
||
|
||
var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
||
console.log('[后视] FBO状态:', fboStatus, '(完整=' + gl.FRAMEBUFFER_COMPLETE + ')');
|
||
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
|
||
|
||
if (fboStatus !== gl.FRAMEBUFFER_COMPLETE) {
|
||
throw new Error('FBO不完整: ' + fboStatus);
|
||
}
|
||
|
||
// 创建 PBO 双缓冲(WebGL2 PIXEL_PACK_BUFFER)
|
||
try {
|
||
var byteSize = rw * rh * 4;
|
||
for (var pi = 0; pi < 2; pi++) {
|
||
pbos[pi] = gl.createBuffer();
|
||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[pi]);
|
||
gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ);
|
||
}
|
||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||
pboSupported = true;
|
||
console.log('[后视] PBO 双缓冲已创建(异步回读)');
|
||
} catch(pboErr) {
|
||
console.warn('[后视] PBO 不支持,回退到同步 readPixels:', pboErr.message);
|
||
pboSupported = false;
|
||
}
|
||
} catch(e) {
|
||
initError = e;
|
||
console.error('[后视] FBO创建失败:', e.message);
|
||
}
|
||
}
|
||
|
||
// ===== Hook gl.bindFramebuffer(后视渲染时重定向到我们的 FBO)=====
|
||
var rearviewRendering = false;
|
||
var mainFbo = null;
|
||
var origBindFramebuffer = null;
|
||
var origH3c = null, origG3c = null;
|
||
|
||
if (gl && !initError) {
|
||
origBindFramebuffer = gl.bindFramebuffer.bind(gl);
|
||
gl.bindFramebuffer = function(target, fbo) {
|
||
// 记录主 FBO(非后视渲染时的绑定)
|
||
if (!rearviewRendering && target === gl.FRAMEBUFFER) {
|
||
mainFbo = fbo;
|
||
}
|
||
// 后视渲染时重定向到我们的 FBO
|
||
if (rearviewRendering && target === gl.FRAMEBUFFER && fbo !== rearFbo) {
|
||
return origBindFramebuffer(target, rearFbo);
|
||
}
|
||
return origBindFramebuffer(target, fbo);
|
||
};
|
||
console.log('[后视] gl.bindFramebuffer 已 Hook');
|
||
|
||
// Hook BS.h3c/g3c:后视渲染时返回 FBO 尺寸(修复 screenSize/aspect ratio)
|
||
// y3s 会调用 BS.h3c()/g3c() 获取 drawingBufferWidth/Height 设置相机 l36_1/m36_1
|
||
// 不 Hook 的话 aspect ratio 会用游戏 canvas 尺寸(如1920x1080),导致画面比例错误
|
||
origH3c = ff.w39_1.o3c.bind(ff.w39_1);
|
||
origG3c = ff.w39_1.n3c.bind(ff.w39_1);
|
||
ff.w39_1.o3c = function() { return rearviewRendering ? rw : origH3c(); };
|
||
ff.w39_1.n3c = function() { return rearviewRendering ? rh : origG3c(); };
|
||
console.log('[后视] BS.o3c/n3c 已 Hook');
|
||
}
|
||
|
||
// ===== 渲染后视镜 =====
|
||
var frameCount = 0;
|
||
var running = true;
|
||
var debugInfo = {
|
||
frame: 0, tankCount: 0, renderedTanks: 0, lastError: null
|
||
};
|
||
|
||
function RenderRearview() {
|
||
if (!running || initError || !ff || !mainCamera || !rootNode || !gl || !rearFbo) return;
|
||
|
||
frameCount++;
|
||
debugInfo.frame = frameCount;
|
||
|
||
// 频率控制
|
||
if (frameCount % (Config.refreshInterval + 1) !== 0) return;
|
||
|
||
var playerTank = GetPlayerTank();
|
||
if (!playerTank) {
|
||
uiCtx.clearRect(0, 0, rw, rh);
|
||
DrawText('未找到玩家坦克', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
|
||
return;
|
||
}
|
||
|
||
var playerPos = GetTankPosition(playerTank);
|
||
if (!playerPos) {
|
||
uiCtx.clearRect(0, 0, rw, rh);
|
||
DrawText('无位置数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
|
||
return;
|
||
}
|
||
|
||
var camInfo = GetCameraInfo();
|
||
if (!camInfo) {
|
||
uiCtx.clearRect(0, 0, rw, rh);
|
||
DrawText('无相机数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
|
||
return;
|
||
}
|
||
|
||
// 后视方向修复:camInfo.yaw = $r.lym_1.h20_1 是 roll(不是 yaw!)
|
||
// o37: forward.y = d[5] = cos(pitch)*cos(roll) + sin(pitch)*sin(yaw)*sin(roll)
|
||
// yaw=π 时 sin(yaw)=0,故后视 forward.y = cos(rearPitch)*cos(roll)
|
||
// roll = camInfo.yaw+π → cos(camInfo.yaw+π) = -cos(camInfo.yaw)
|
||
// 故后视 forward.y = -cos(rearPitch)*cos(camInfo.yaw),主视为 +cos(pitch)*cos(camInfo.yaw)
|
||
// → 后视水平方向 = -主视水平方向(朝后看 ✓,与 rearPitch 符号无关)
|
||
// o37: forward.z = d[9] = sin(pitch)*cos(yaw)
|
||
// 后视 yaw=π: forward.z = -sin(rearPitch)
|
||
// 要后视与主视同垂直方向(低头时都朝下看地形):rearPitch = -camInfo.pitch
|
||
// → 后视 forward.z = -sin(-camInfo.pitch) = sin(camInfo.pitch) = 主视 forward.z ✓
|
||
// up 向量:rearPitch=-camInfo.pitch 时 up.z<0(朝下),FBO 画面颠倒
|
||
// GL readPixels(自底向上) + putImageData(自顶向下) = Y 翻转,使显示画面正立 ✓
|
||
var rearPitch = -camInfo.pitch;
|
||
|
||
// 后视相机位置:玩家位置 + 高度偏移
|
||
var camPosX = playerPos.x;
|
||
var camPosY = playerPos.y;
|
||
var camPosZ = playerPos.z + Config.cameraHeight;
|
||
|
||
// ===== 保存主相机参数 =====
|
||
var savedPitch = mainCamera.u35_1.f20_1;
|
||
var savedYaw = mainCamera.u35_1.g20_1;
|
||
var savedRoll = mainCamera.u35_1.h20_1;
|
||
var savedX = mainCamera.r35_1.f20_1;
|
||
var savedY = mainCamera.r35_1.g20_1;
|
||
var savedZ = mainCamera.r35_1.h20_1;
|
||
var savedL36 = mainCamera.u36_1;
|
||
var savedM36 = mainCamera.v36_1;
|
||
var savedS36 = mainCamera.z36_1;
|
||
var savedQ36 = mainCamera.x36_1;
|
||
var savedR36 = mainCamera.y36_1;
|
||
|
||
// 临时暂停自由视角的 h37 hook,避免覆盖后视镜相机参数
|
||
var fl = window.__hornetFreeLook__;
|
||
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true);
|
||
|
||
try {
|
||
// ===== 设置后视相机参数 =====
|
||
// g=π 使水平方向反向(朝后看),h=yaw+π 保持水平视角正确
|
||
// f=-pitch 使垂直方向与主视一致(低头看后方地形)
|
||
// up 朝下 → FBO 画面颠倒 → GL readback Y 翻转修正
|
||
mainCamera.u35_1.f20_1 = rearPitch;
|
||
mainCamera.u35_1.g20_1 = Math.PI;
|
||
mainCamera.u35_1.h20_1 = camInfo.yaw + Math.PI;
|
||
mainCamera.r35_1.f20_1 = camPosX;
|
||
mainCamera.r35_1.g20_1 = camPosY;
|
||
mainCamera.r35_1.h20_1 = camPosZ;
|
||
mainCamera.u36_1 = rw;
|
||
mainCamera.v36_1 = rh;
|
||
mainCamera.z36_1 = Config.fov;
|
||
mainCamera.x36_1 = Config.near;
|
||
mainCamera.y36_1 = Config.far;
|
||
|
||
// 更新相机矩阵(viewMatrix, projectionMatrix, viewProjectionMatrix)
|
||
mainCamera.o37();
|
||
|
||
// 保存后视视图矩阵副本(用于 UI 标记投影,finally 会恢复相机所以需提前保存)
|
||
var rearViewMatrix = new Float32Array(mainCamera.b37_1);
|
||
|
||
// ===== 保存并禁用阴影/后处理(避免它们切换 FBO 覆盖 rearFbo)=====
|
||
// p38_1.b3o_1 控制 z39_1(阴影),c3o_1 控制 c3a_1/d3a_1(后处理),z3n_1 控制 f3a_1
|
||
// 禁用后 y3s 只执行主场景渲染 z3k,跳过所有 FBO 切换的 pass
|
||
var savedX3n = ff.y38_1.e3o_1;
|
||
var savedY3n = ff.y38_1.f3o_1;
|
||
var savedZ3n = ff.y38_1.k3o_1;
|
||
ff.y38_1.e3o_1 = false;
|
||
ff.y38_1.f3o_1 = false;
|
||
ff.y38_1.k3o_1 = false;
|
||
|
||
// ===== 禁用 UI 渲染层(防止技能条等 UI 混入后视 FBO)=====
|
||
// 保存 UI 层队列,替换为空假对象,y3s 后恢复
|
||
var savedUiQueues = {};
|
||
for (var ui = 0; ui < uiLayerIndices.length; ui++) {
|
||
var li = uiLayerIndices[ui];
|
||
savedUiQueues[li] = ff.e39_1[li];
|
||
ff.e39_1[li] = fakeQueue;
|
||
}
|
||
|
||
// ===== 渲染到 FBO =====
|
||
rearviewRendering = true;
|
||
|
||
// 清除 FBO
|
||
origBindFramebuffer(gl.FRAMEBUFFER, rearFbo);
|
||
gl.viewport(0, 0, rw, rh);
|
||
gl.clearColor(0.15, 0.18, 0.22, 1.0);
|
||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||
|
||
// 调用游戏原生渲染器(已禁用阴影/后处理/UI层,只渲染 3D 场景)
|
||
ff.f3t(rootNode, mainCamera);
|
||
|
||
rearviewRendering = false;
|
||
|
||
// ===== Readback FBO 到 canvas(PBO 异步回读,避免 GPU 管线停顿)=====
|
||
origBindFramebuffer(gl.FRAMEBUFFER, rearFbo);
|
||
|
||
if (pboSupported) {
|
||
// PBO 异步回读:当前帧发起异步 readPixels 到 PBO[current],读取上一帧的 PBO[previous] 数据
|
||
var curPbo = pboIndex;
|
||
var prevPbo = 1 - pboIndex;
|
||
|
||
// 发起当前帧的异步 readPixels(写入 PBO,不阻塞 GPU)
|
||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]);
|
||
gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, 0);
|
||
|
||
// 读取上一帧的 PBO 数据(此时 GPU 应已完成)
|
||
if (pboReady) {
|
||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]);
|
||
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels);
|
||
}
|
||
|
||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||
pboIndex = prevPbo;
|
||
pboReady = true;
|
||
} else {
|
||
// 回退:同步 readPixels
|
||
gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
}
|
||
|
||
// 显示画面(g=π 渲染已自带 Y 翻转,直接复制)
|
||
// 修复半透明边缘:强制 alpha=255(树叶等 alpha 混合会导致 canvas 透明)
|
||
for (var ai = 0; ai < pixels32.length; ai++) {
|
||
pixels32[ai] = pixels32[ai] | 0xFF000000; // little-endian: 高字节=alpha
|
||
}
|
||
imageData.data.set(pixels);
|
||
uiCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 恢复主 FBO
|
||
origBindFramebuffer(gl.FRAMEBUFFER, mainFbo);
|
||
|
||
} catch(e) {
|
||
debugInfo.lastError = e.message;
|
||
console.error('[后视] 渲染失败:', e.message, e.stack);
|
||
uiCtx.clearRect(0, 0, rw, rh);
|
||
DrawText('渲染错误: ' + e.message, rw/2, rh/2, 'rgba(255,100,100,1)', '11px monospace', 'center');
|
||
} finally {
|
||
// ===== 确保 rearviewRendering 已重置 =====
|
||
rearviewRendering = false;
|
||
|
||
// ===== 恢复阴影/后处理(异常时也要恢复)=====
|
||
try {
|
||
if (typeof savedX3n !== 'undefined') {
|
||
ff.y38_1.e3o_1 = savedX3n;
|
||
ff.y38_1.f3o_1 = savedY3n;
|
||
ff.y38_1.k3o_1 = savedZ3n;
|
||
}
|
||
} catch(e) {
|
||
console.error('[后视] 恢复阴影/后处理失败:', e.message);
|
||
}
|
||
|
||
// ===== 恢复 UI 渲染层(异常时也要恢复,否则游戏 UI 永久失效)=====
|
||
try {
|
||
if (savedUiQueues) {
|
||
for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) {
|
||
var li2 = uiLayerIndices[ui2];
|
||
ff.e39_1[li2] = savedUiQueues[li2];
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('[后视] 恢复 UI 层失败:', e.message);
|
||
}
|
||
|
||
// ===== 恢复主相机参数 =====
|
||
try {
|
||
mainCamera.u35_1.f20_1 = savedPitch;
|
||
mainCamera.u35_1.g20_1 = savedYaw;
|
||
mainCamera.u35_1.h20_1 = savedRoll;
|
||
mainCamera.r35_1.f20_1 = savedX;
|
||
mainCamera.r35_1.g20_1 = savedY;
|
||
mainCamera.r35_1.h20_1 = savedZ;
|
||
mainCamera.u36_1 = savedL36;
|
||
mainCamera.v36_1 = savedM36;
|
||
mainCamera.z36_1 = savedS36;
|
||
mainCamera.x36_1 = savedQ36;
|
||
mainCamera.y36_1 = savedR36;
|
||
mainCamera.o37();
|
||
} catch(e) {
|
||
console.error('[后视] 恢复相机失败:', e.message);
|
||
}
|
||
// 恢复自由视角的 h37 hook
|
||
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false);
|
||
}
|
||
|
||
// ===== 绘制 UI 标记(坦克位置标记)=====
|
||
if (rearViewMatrix) {
|
||
DrawUIMarkers(playerTank, playerPos, camInfo, rearViewMatrix);
|
||
}
|
||
}
|
||
|
||
// ===== UI 标记绘制 =====
|
||
function DrawUIMarkers(playerTank, playerPos, camInfo, viewMatrix) {
|
||
var tankList = gm.m10b_1.j11y();
|
||
var count = tankList.e1();
|
||
debugInfo.tankCount = count;
|
||
var visibleTanks = 0;
|
||
|
||
for (var i = 0; i < count; i++) {
|
||
var tank = tankList.g1(i);
|
||
if (!tank || tank === playerTank) continue;
|
||
if (!IsTankAlive(tank)) continue;
|
||
|
||
var pos = GetTankPosition(tank);
|
||
if (!pos) continue;
|
||
|
||
// 使用视图矩阵投影(与 3D 渲染一致)
|
||
// o37 变换前矩阵行:right(d[0,4,8]), forward(d[1,5,9]), up(d[2,6,10])
|
||
// Ua 变换后行:right, up, -forward
|
||
// 故变换后相机空间:camX=右,camY=上(up·rel),camZ=深度(-forward·rel,负=前方)
|
||
// 后视相机 up 朝下:世界"上"rel.z>0 → camY<0;渲染+FBO readback 使世界"上"显示在 canvas 顶部
|
||
// marker 用 + 号匹配:camY<0(上方) → +负 → screen.y<rh/2 → canvas 顶部
|
||
var wx = pos.x, wy = pos.y, wz = pos.z;
|
||
var m = viewMatrix;
|
||
var camX = m[0]*wx + m[4]*wy + m[8]*wz + m[12];
|
||
var camY = m[1]*wx + m[5]*wy + m[9]*wz + m[13]; // 上:up·rel
|
||
var camZ = m[2]*wx + m[6]*wy + m[10]*wz + m[14]; // 深度:-forward·rel,负=前方
|
||
if (camZ >= -0.1) continue; // 剔除相机后方(深度非负)
|
||
var depth = -camZ; // 正值=距离
|
||
var focalLength = rh / (2 * Math.tan(Config.fov / 2));
|
||
var screen = {
|
||
x: rw / 2 + (camX / depth) * focalLength,
|
||
y: rh / 2 + (camY / depth) * focalLength, // +号匹配 GL readback Y 翻转后的画面
|
||
depth: depth
|
||
};
|
||
|
||
if (screen.x < 0 || screen.x > rw || screen.y < 0 || screen.y > rh) continue;
|
||
if (screen.depth > Config.maxRange) continue;
|
||
|
||
visibleTanks++;
|
||
var friendly = IsFriendly(tank, playerTank);
|
||
var color = friendly ? 'rgba(80,180,255,1)' : 'rgba(255,80,60,1)';
|
||
var bgColor = friendly ? 'rgba(40,90,140,0.7)' : 'rgba(140,40,30,0.7)';
|
||
|
||
// 方框标记
|
||
uiCtx.strokeStyle = color;
|
||
uiCtx.lineWidth = 1.5;
|
||
uiCtx.strokeRect(screen.x - 6, screen.y - 6, 12, 12);
|
||
|
||
// 名称
|
||
var name = GetTankName(tank);
|
||
if (name) {
|
||
DrawTextOutlined(name, screen.x, screen.y - 12, color, '10px monospace', 'center');
|
||
}
|
||
|
||
// 血量条
|
||
var health = GetTankHealth(tank);
|
||
if (health) {
|
||
var barW = 24, barH = 3;
|
||
var barX = screen.x - barW / 2, barY = screen.y + 8;
|
||
uiCtx.fillStyle = 'rgba(0,0,0,0.6)';
|
||
uiCtx.fillRect(barX, barY, barW, barH);
|
||
var hpColor = health.percent > 0.6 ? 'rgba(80,255,80,1)' :
|
||
health.percent > 0.3 ? 'rgba(255,200,60,1)' : 'rgba(255,80,60,1)';
|
||
uiCtx.fillStyle = hpColor;
|
||
uiCtx.fillRect(barX, barY, barW * health.percent, barH);
|
||
}
|
||
}
|
||
|
||
debugInfo.renderedTanks = visibleTanks;
|
||
|
||
// 边框 + 标题
|
||
uiCtx.strokeStyle = 'rgba(120,200,255,0.4)';
|
||
uiCtx.lineWidth = 1;
|
||
uiCtx.strokeRect(0, 0, rw, rh);
|
||
|
||
DrawTextOutlined('后视镜', 8, 12, 'rgba(120,200,255,0.9)', '11px monospace', 'left');
|
||
DrawTextOutlined(visibleTanks + '/' + count, rw - 8, 12, 'rgba(180,180,180,0.9)', '11px monospace', 'right');
|
||
|
||
if (debugInfo.lastError) {
|
||
DrawTextOutlined('ERR: ' + debugInfo.lastError.substring(0, 30), 8, rh - 8, 'rgba(255,100,100,0.9)', '10px monospace', 'left');
|
||
}
|
||
}
|
||
|
||
// ===== 主循环 =====
|
||
function Loop() {
|
||
if (!running) return;
|
||
|
||
// 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook
|
||
var NewGm = window.__gameMode__;
|
||
if (NewGm && NewGm !== gm) {
|
||
console.log('[后视] 检测到新战斗,正在重启后视镜...');
|
||
Stop();
|
||
_start();
|
||
return;
|
||
}
|
||
|
||
requestAnimationFrame(Loop);
|
||
// 旧实例已失效、新实例尚未捕获:跳过本帧
|
||
if (!window.__gameMode__) return;
|
||
RenderRearview();
|
||
}
|
||
|
||
// ===== 停止函数 =====
|
||
function Stop() {
|
||
running = false;
|
||
console.log('[后视] 停止后视镜');
|
||
|
||
// 恢复 gl.bindFramebuffer
|
||
if (origBindFramebuffer && gl) {
|
||
gl.bindFramebuffer = origBindFramebuffer;
|
||
console.log('[后视] gl.bindFramebuffer 已恢复');
|
||
}
|
||
|
||
// 恢复 BS.h3c/g3c
|
||
if (origH3c && ff && ff.w39_1) {
|
||
ff.w39_1.o3c = origH3c;
|
||
ff.w39_1.n3c = origG3c;
|
||
console.log('[后视] BS.o3c/n3c 已恢复');
|
||
}
|
||
|
||
// 清理 FBO
|
||
if (gl && rearFbo) gl.deleteFramebuffer(rearFbo);
|
||
if (gl && rearTex) gl.deleteTexture(rearTex);
|
||
if (gl && rearDepthRb) gl.deleteRenderbuffer(rearDepthRb);
|
||
|
||
// 清理 PBO
|
||
if (gl && pbos[0]) gl.deleteBuffer(pbos[0]);
|
||
if (gl && pbos[1]) gl.deleteBuffer(pbos[1]);
|
||
|
||
// 移除 canvas
|
||
if (uiCanvas && uiCanvas.parentNode) {
|
||
uiCanvas.parentNode.removeChild(uiCanvas);
|
||
}
|
||
|
||
window.__hornetRearview__ = null;
|
||
}
|
||
|
||
// ===== 启动 =====
|
||
window.__hornetRearview__ = {
|
||
Stop: Stop, Config: Config,
|
||
setFov: function(f) {
|
||
var n = parseFloat(f);
|
||
if (!isNaN(n) && n >= 0.5 && n <= 2.5) {
|
||
Config.fov = n;
|
||
try { localStorage.setItem('hornet_rearview_fov', String(n)); } catch(e) {}
|
||
console.log('[后视] FOV 设为: ' + n);
|
||
} else {
|
||
console.warn('[后视] FOV 应在 0.5~2.5');
|
||
}
|
||
},
|
||
getFov: function() { return Config.fov; },
|
||
setOpacity: function(o) {
|
||
var n = parseFloat(o);
|
||
if (!isNaN(n) && n >= 0.2 && n <= 1) {
|
||
RearviewOpacity = n;
|
||
uiCanvas.style.opacity = String(n);
|
||
try { localStorage.setItem('hornet_rearview_opacity', String(n)); } catch(e) {}
|
||
} else {
|
||
console.warn('[后视] 透明度应在 0.2~1');
|
||
}
|
||
},
|
||
getOpacity: function() { return RearviewOpacity; }
|
||
};
|
||
|
||
if (initError) {
|
||
uiCtx.clearRect(0, 0, rw, rh);
|
||
DrawText('初始化失败:', rw/2, rh/2 - 10, 'rgba(255,100,100,1)', '13px monospace', 'center');
|
||
DrawText(initError.message, rw/2, rh/2 + 10, 'rgba(255,200,80,1)', '11px monospace', 'center');
|
||
console.error('[后视] 初始化失败,请检查字段访问');
|
||
return;
|
||
}
|
||
|
||
console.log('[后视] 原生渲染后视镜已启动');
|
||
console.log('[后视] 配置: ' + rw + 'x' + rh + ', FOV=' + Config.fov + ', 间隔=' + Config.refreshInterval);
|
||
Loop();
|
||
}
|
||
|
||
var Tool = {
|
||
Start: function() {
|
||
if (window.__hornetRearview__) return;
|
||
_start();
|
||
},
|
||
Stop: function() {
|
||
if (window.__hornetRearview__) window.__hornetRearview__.Stop();
|
||
},
|
||
Status: function() {
|
||
return window.__hornetRearview__ ? '运行中' : '已停止';
|
||
},
|
||
GetRunning: function() {
|
||
return !!window.__hornetRearview__;
|
||
},
|
||
SetFov: function(f) { if (window.__hornetRearview__) window.__hornetRearview__.setFov(f); },
|
||
GetFov: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getFov() : 1.2; },
|
||
SetOpacity: function(o) { if (window.__hornetRearview__) window.__hornetRearview__.setOpacity(o); },
|
||
GetOpacity: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getOpacity() : 1.0; }
|
||
};
|
||
|
||
window.__hornetTools__ = window.__hornetTools__ || {};
|
||
window.__hornetTools__.Rearview = Tool;
|
||
console.log('[3DTank] 后视镜模块已加载');
|
||
})();
|