diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 9ebd350..a61dbec 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -488,14 +488,20 @@ struct VFrame { bool has_cam; bool ydown; /* game world is y-down (DCS reflections) */ float rot[9], eye[3]; /* row-major rotation; eye = R*(p - e) */ + /* real wire lights (dpl3-revive: lmodel type-2 ambient + type-3 dir suns). + * samb = summed scene ambient; nlit dir lights (dir toward light + col). */ + float samb[3]; + int nlit; + float ldir[2][3], lcol[2][3]; std::vector polys; VFrame() : valid(false), nearp(2), farp(12000), fog(false), fogn(0), fogf(0), vw(832), vh(512), - has_cam(false), ydown(false) { + has_cam(false), ydown(false), nlit(0) { bg[0] = bg[1] = bg[2] = 0; fogc[0] = fogc[1] = fogc[2] = 0; win[0] = -1; win[1] = -0.6153846f; win[2] = 1; win[3] = 0.6153846f; win[4] = 1.3f; + samb[0] = samb[1] = samb[2] = 0.0f; } }; @@ -515,8 +521,14 @@ static struct VScene { std::map > children; /* game (full DPL) hierarchy: instance placement + articulation */ std::map inst_object; /* instance -> object */ + std::map inst_w3; /* instance -> display mode word3 */ std::map dcs_mat; /* dcs -> local matrix */ std::map dcs_parent; /* dcs_link child->parent */ + /* wire lights: dpl3-revive decode -- lmodel (type 6) / light (type 0xe) + * body: dcs @d+12, light_type @d+16 (2=ambient, 3=directional), rgb + * @d+20..28. directional aim = the light DCS's +Z world row. */ + struct VLight { unsigned dcs; int ltype; float rgb[3]; }; + std::map lights; /* light node -> params */ /* texture chain: material -> texmap -> texture, colorized by the * material's ramp (type 14: lo/hi RGB; texels are intensities) */ std::map tex; /* texture node -> texels */ @@ -639,24 +651,34 @@ static const char *rsh_vs_src = " vUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;\n" " gl_Position = gl_ProjectionMatrix * v;\n" "}\n"; +/* Shading model ported 1:1 from the dpl3-revive software rasterizer + * (patha/vrview.py draw(), validated against our own arena captures): + * lit = sceneAmbient + sum_i |dot(N, Ldir_i)| * Lcol_i (DPL double-sided) + * base = matEmis + matDiff * lit + * c = hasTex ? tex * base * 1.275 : base (tex*base/200, base was *255) + * out = pow(fog(c), 1/1.25) (Division 10-bit-DAC gamma) + * Material ambient and the light-ramp are intentionally unused here -- the + * proven Division look folds ambient into the diffuse response and does not + * ramp the base colour (the ramp only re-colours intensity texels at bake). */ static const char *rsh_fs_src = "varying vec3 vN; varying vec2 vUV; varying float vDist;\n" - "uniform vec3 uAmbScene, uMatAmb, uMatDiff, uMatEmis, uFogColor, uViewFwd;\n" + "uniform vec3 uAmbScene, uMatDiff, uMatEmis, uFogColor;\n" "uniform vec3 uLightDir0, uLightCol0, uLightDir1, uLightCol1;\n" - "uniform vec3 uRamp0, uRamp1, uFog;\n" /* uFog: start, end, immune */ + "uniform vec3 uFog;\n" /* uFog: start, end, immune */ "uniform float uHasTex; uniform sampler2D uTex;\n" "void main() {\n" " vec3 N = normalize(vN);\n" - " if (dot(N, uViewFwd) > 0.0) N = -N;\n" - " vec3 acc = uLightCol0 * max(dot(N, -uLightDir0), 0.0)\n" - " + uLightCol1 * max(dot(N, -uLightDir1), 0.0);\n" - " vec3 lit = mix(uRamp0, uRamp1, clamp(acc, 0.0, 1.0));\n" - " vec3 tex = mix(vec3(1.0), texture2D(uTex, vUV).rgb, uHasTex);\n" - " vec3 c = (uMatAmb * uAmbScene + uMatDiff * lit) * tex + uMatEmis;\n" + " vec3 lit = uAmbScene\n" + " + uLightCol0 * abs(dot(N, uLightDir0))\n" + " + uLightCol1 * abs(dot(N, uLightDir1));\n" + " vec3 base = uMatEmis + uMatDiff * lit;\n" + " vec3 tex = texture2D(uTex, vUV).rgb;\n" + " vec3 c = mix(base, tex * base * 1.275, uHasTex);\n" " float fr = clamp((uFog.y - vDist) / max(uFog.y - uFog.x, 0.001),\n" " 0.0, 1.0);\n" " fr = max(fr, uFog.z);\n" - " gl_FragColor = vec4(mix(uFogColor, c, fr), 1.0);\n" + " c = mix(uFogColor, c, fr);\n" + " gl_FragColor = vec4(pow(clamp(c, 0.0, 1.0), vec3(1.0/1.25)), 1.0);\n" "}\n"; static GLuint rsh_compile(GLenum kind, const char *src, @@ -731,15 +753,17 @@ static void rsh_init(void) { u1i(gu(pr, "uTex"), 0); rsh.use(0); rsh.prog = pr; - fprintf(stderr, "VPX render: gallery shading model active (GLSL)\n"); + fprintf(stderr, "VPX render: dpl3-revive shading model active (GLSL)\n"); } /* scene lighting until the type-7 light node decode lands (step 2): * defaults keep the previous hardcoded sun; VPX_AMBIENT="r,g,b" and * VPX_SUN="r,g,b,pitch,yaw" (degrees, y-down world) override for tuning */ -static float rsh_amb[3] = { 0.45f, 0.45f, 0.45f }; +/* dpl3-revive default light rig (vrview.py): scene ambient 0.35 + one sun 0.65 + * when the wire carries no lights (real light-node decode = next increment). */ +static float rsh_amb[3] = { 0.35f, 0.35f, 0.35f }; static float rsh_sundir[3] = { -0.3412f, 0.8286f, -0.3899f }; /* travel dir */ -static float rsh_suncol[3] = { 0.80f, 0.80f, 0.78f }; +static float rsh_suncol[3] = { 0.65f, 0.65f, 0.65f }; static void rsh_env(void) { static bool done = false; if (done) return; @@ -769,10 +793,13 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { double wd = f.win[4] != 0 ? f.win[4] : 1.3; glMatrixMode(GL_PROJECTION); glLoadIdentity(); - /* Division screen x runs opposite to GL eye x (SMPTE pattern comes - * out mirrored otherwise) -- flip x after projection. The game world - * is additionally y-down (its DCS matrices carry a reflection). */ - glScalef(-1.0f, f.ydown ? -1.0f : 1.0f, 1.0f); + /* Demo/test path: Division screen x runs opposite to GL eye x + * (SMPTE pattern comes out mirrored otherwise) -- flip x after + * projection. Game path (ydown): NO x flip -- dpl3-revive's proven + * pipeline has no lateral mirror, and the flip made yaw read + * inverted (turn right, view swings left); the world's y-down DCS + * reflection still needs the y flip. */ + glScalef(f.ydown ? 1.0f : -1.0f, f.ydown ? -1.0f : 1.0f, 1.0f); glFrustum(f.win[0] * n / wd, f.win[2] * n / wd, f.win[1] * n / wd, f.win[3] * n / wd, n, fa); glMatrixMode(GL_MODELVIEW); @@ -801,13 +828,37 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { glDisable(GL_FOG); glDisable(GL_LIGHTING); rsh.use(rsh.prog); - rsh.u3f(rsh.uAmbScene, rsh_amb[0], rsh_amb[1], rsh_amb[2]); - rsh.u3f(rsh.uLightDir0, rsh_sundir[0], rsh_sundir[1], - rsh_sundir[2]); - rsh.u3f(rsh.uLightCol0, rsh_suncol[0], rsh_suncol[1], - rsh_suncol[2]); - rsh.u3f(rsh.uLightDir1, 0.0f, 1.0f, 0.0f); - rsh.u3f(rsh.uLightCol1, 0.0f, 0.0f, 0.0f); + /* real wire lights (lmodel) when the frame carries any; else the + * dpl3-revive default rig (scene ambient 0.35 + one sun 0.65). */ + bool wl = f.nlit > 0 || + (f.samb[0] + f.samb[1] + f.samb[2]) > 0.0f; + if (wl) { + rsh.u3f(rsh.uAmbScene, f.samb[0], f.samb[1], f.samb[2]); + rsh.u3f(rsh.uLightDir0, + f.nlit >= 1 ? f.ldir[0][0] : 0.0f, + f.nlit >= 1 ? f.ldir[0][1] : 1.0f, + f.nlit >= 1 ? f.ldir[0][2] : 0.0f); + rsh.u3f(rsh.uLightCol0, + f.nlit >= 1 ? f.lcol[0][0] : 0.0f, + f.nlit >= 1 ? f.lcol[0][1] : 0.0f, + f.nlit >= 1 ? f.lcol[0][2] : 0.0f); + rsh.u3f(rsh.uLightDir1, + f.nlit >= 2 ? f.ldir[1][0] : 0.0f, + f.nlit >= 2 ? f.ldir[1][1] : 1.0f, + f.nlit >= 2 ? f.ldir[1][2] : 0.0f); + rsh.u3f(rsh.uLightCol1, + f.nlit >= 2 ? f.lcol[1][0] : 0.0f, + f.nlit >= 2 ? f.lcol[1][1] : 0.0f, + f.nlit >= 2 ? f.lcol[1][2] : 0.0f); + } else { + rsh.u3f(rsh.uAmbScene, rsh_amb[0], rsh_amb[1], rsh_amb[2]); + rsh.u3f(rsh.uLightDir0, rsh_sundir[0], rsh_sundir[1], + rsh_sundir[2]); + rsh.u3f(rsh.uLightCol0, rsh_suncol[0], rsh_suncol[1], + rsh_suncol[2]); + rsh.u3f(rsh.uLightDir1, 0.0f, 1.0f, 0.0f); + rsh.u3f(rsh.uLightCol1, 0.0f, 0.0f, 0.0f); + } /* camera forward in world coords: -row2 of the world->eye * rotation (rows = eye axes in world; eye looks down -z) */ rsh.u3f(rsh.uViewFwd, -f.rot[6], -f.rot[7], -f.rot[8]); @@ -866,8 +917,10 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { if (gi == gltex.end()) { glGenTextures(1, &id); gltex[it->first] = id; } else id = gi->second; glBindTexture(GL_TEXTURE_2D, id); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + /* the i860 board point-sampled (VRENDER SCANLINE.SS: one fld.l per + * pixel, no bilinear tap) -- NEAREST is authentic (dpl3-revive) */ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, it->second.w, it->second.h, @@ -1420,6 +1473,48 @@ static void scene_publish_frame(void) { * dcs_link articulation tree. */ std::map cache; std::map gg_done; + /* resolve wire lights: ambient (type 2) sums into samb; directional + * (type 3) aim = the light DCS's +Z world row (dpl3-revive). Up to two + * directional slots feed the shader; if none, rt_draw uses its rig. */ + for (std::map::const_iterator li = + S.lights.begin(); li != S.lights.end(); ++li) { + const VScene::VLight &L = li->second; + if (L.ltype == 2) { + for (int i = 0; i < 3; i++) f.samb[i] += L.rgb[i]; + } else if (L.ltype == 3 && f.nlit < 2) { + M16 w; + dcs_world(L.dcs, cache, w); + float dx = w.m[8], dy = w.m[9], dz = w.m[10]; /* +Z world row */ + float n = sqrtf(dx * dx + dy * dy + dz * dz); + if (n > 1e-6f) { + int s = f.nlit++; + f.ldir[s][0] = dx / n; f.ldir[s][1] = dy / n; + f.ldir[s][2] = dz / n; + for (int i = 0; i < 3; i++) f.lcol[s][i] = L.rgb[i]; + } + } + } + { /* periodic light-decode diagnostic (stderr): how many type-6/0xe + * light nodes exist vs how many decoded, sampled throughout so it + * catches the mission-loaded scene (not just boot). */ + static int fc = 0; + if (++fc % 120 == 1) { + int n6 = 0, ne = 0; + for (std::map::const_iterator ti = + S.type.begin(); ti != S.type.end(); ++ti) { + if (ti->second == 6) n6++; + if (ti->second == 0xe) ne++; + } + fprintf(stderr, "VPX lights@f%d: type6=%d typeE=%d decoded=%d " + "dir=%d amb(%.2f,%.2f,%.2f)\n", fc, n6, ne, + (int)S.lights.size(), f.nlit, + f.samb[0], f.samb[1], f.samb[2]); + if (f.nlit) + fprintf(stderr, " L0 dir(%.2f,%.2f,%.2f) col(%.2f,%.2f,%.2f)\n", + f.ldir[0][0], f.ldir[0][1], f.ldir[0][2], + f.lcol[0][0], f.lcol[0][1], f.lcol[0][2]); + } + } for (std::map >::const_iterator di = S.children.begin(); di != S.children.end(); ++di) { if (S.type.count(di->first) == 0 || S.type[di->first] != 5) continue; @@ -1427,6 +1522,8 @@ static void scene_publish_frame(void) { dcs_world(di->first, cache, world); for (size_t i = 0; i < di->second.size(); i++) { unsigned inst = di->second[i]; + /* (hidden-instance w3 skip removed -- our w3 offset was culling + * the ground plane; revisit once the offset is confirmed) */ std::map::const_iterator oi = S.inst_object.find(inst); if (oi == S.inst_object.end()) continue; @@ -1564,6 +1661,13 @@ static void scene_burst(const unsigned char *p, size_t n) { float op = rd_f32(d + 60), sh = rd_f32(d + 84); m.opacity = (op >= 0.0f && op <= 1.0f) ? op : 1.0f; m.shin = (sh >= 0.0f && sh <= 1024.0f) ? sh : 0.0f; + /* dpl3-revive: exact ambient==(1,0,0) AND diffuse==(1,0,0) is + * the shipped build's UNSET-material marker (on every + * .B2Z-default material), not a colour -- render as white. */ + if (m.amb[0] == 1.0f && m.amb[1] == 0.0f && m.amb[2] == 0.0f && + m.diff[0] == 1.0f && m.diff[1] == 0.0f && m.diff[2] == 0.0f) { + for (int i = 0; i < 3; i++) { m.amb[i] = 1.0f; m.diff[i] = 1.0f; } + } S.mat[name] = m; unsigned tref = rd_u32(d + 8), rref = rd_u32(d + 20); if (tref && tref != 0xFFFFFFFFu) S.mat_texmap[name] = tref; @@ -1602,11 +1706,38 @@ static void scene_burst(const unsigned char *p, size_t n) { fprintf(tl, "\n"); fflush(tl); } - } else if ((t == 6 || t == 2) && nb >= 8) { /* lmodel / zone */ + } else if (t == 6 || t == 0xe) { /* lmodel / light */ + { static bool dl = false; if (!dl) { dl = true; + fprintf(stderr, "VPX lightnode t=%u nb=%lu:", + (unsigned)t, (unsigned long)nb); + for (size_t o = 8; o + 3 < nb && o < 56; o += 4) + fprintf(stderr, " [%u]%08x/%g", (unsigned)o, + rd_u32(d + o), rd_f32(d + o)); + fprintf(stderr, "\n"); } } + /* dpl3-revive light decode: dcs @d+12, light_type @d+16 + * (2=ambient, 3=directional), rgb @d+20..28. Directional aim + * is resolved to the light DCS's +Z world row at frame + * assembly. The sanity range rejects the game's 4 stray + * type-0xe 32B "vehicle lamp" bodies (heap-garbage rgb). */ + if (nb >= 32) { + unsigned ldcs = rd_u32(d + 12); + int ltype = (int)rd_u32(d + 16); + float r = rd_f32(d + 20), g = rd_f32(d + 24), + b = rd_f32(d + 28); + if ((ltype == 2 || ltype == 3) && + r == r && g == g && b == b && /* NaN guard */ + r >= 0.0f && g >= 0.0f && b >= 0.0f && + r <= 100.0f && g <= 100.0f && b <= 100.0f) { + VScene::VLight L; + L.dcs = ldcs; L.ltype = ltype; + L.rgb[0] = r; L.rgb[1] = g; L.rgb[2] = b; + S.lights[name] = L; + } + } + } else if (t == 2 && nb >= 8) { /* zone (raw log) */ FILE *tl = tex_log_fp(); if (tl) { - fprintf(tl, "t%d %s=%08x raw=", (int)t, - t == 6 ? "lmodel" : "zone", name); + fprintf(tl, "t2 zone=%08x raw=", name); for (size_t o = 8; o + 3 < nb && o < 40; o += 4) fprintf(tl, " %08x", rd_u32(d + o)); fprintf(tl, "\n"); @@ -1670,6 +1801,10 @@ static void scene_burst(const unsigned char *p, size_t n) { for (int r = 0; r < 3; r++) mm.m[r * 4 + 3] = 0.0f; mm.m[15] = 1.0f; } else if (t == 4) { /* instance: object ref */ + /* display mode (dpl3-revive): word3 @d+16 -- 3=normal, + * 2=billboard, 0/1=HIDDEN until armed (the parked player mech + * is w3=1 pre-translocation). Stored for the assembly skip. */ + if (nb >= 20) S.inst_w3[name] = rd_u32(d + 16); for (size_t o = 8; o + 3 < nb; o += 4) { unsigned val = rd_u32(d + o); if (val && val != 0xFFFFFFFFu) { @@ -1753,7 +1888,8 @@ static void scene_burst(const unsigned char *p, size_t n) { static void scene_reset(void) { S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear(); S.ggmat.clear(); S.children.clear(); - S.inst_object.clear(); S.dcs_mat.clear(); S.dcs_parent.clear(); + S.inst_object.clear(); S.inst_w3.clear(); S.dcs_mat.clear(); + S.dcs_parent.clear(); S.lights.clear(); S.view = VFrame(); S.geom_active = false; S.conn_active = false; S.geom_acc.clear();