c/3d/gamma_demo.c · 30276 字节 · 下载原文
/* gamma_demo.c —— **3D 渲染能力第一批**(伽马/色调映射 · alpha test · mipmap+采样器)示例 + 判据。
* gamma_demo.c — **first batch of 3D render features** (gamma/tone mapping, alpha test,
* mipmap+sampler): example and criteria.
*
* ## 这个文件覆盖三样(按序号 `#649`–`#651`)
* ## This file covers three things (numbers #649-#651)
*
* ① `ui_3d_viewport_set_gamma(vp, mode)` `#649` —— 输出变换(sRGB / Reinhard / ACES)
* 1) `ui_3d_viewport_set_gamma(vp, mode)` `#649` - output transform (sRGB / Reinhard / ACES)
* ② `ui_3d_obj_alpha_test(obj, cutoff)` `#650` —— 镂空(glTF 的 `alphaMode: MASK`)
* 2) `ui_3d_obj_alpha_test(obj, cutoff)` `#650` - alpha test (glTF's alphaMode: MASK)
* ③ `ui_3d_obj_sampler(obj,ws,wt,min,mag)` `#651` —— mipmap + 过滤 + 平铺
* 3) `ui_3d_obj_sampler(obj,ws,wt,min,mag)` `#651` - mipmap + filtering + wrap
*
* ## ⚠⚠ 三样**默认全关**,而这个文件必须能证明「关着时真的没动」
* ## ⚠⚠ All three are **off by default**, and this file must prove "off really means untouched"
*
* 本仓对老程序有一条硬保证:只调过 `#612` 的程序画面**逐位不变**。
* The engine guarantees old programs are **bit-identical**.
* 这三样都会改像素(伽马改全屏、mipmap 改远景、alpha test 改镂空),所以每段判据都配一条**反向**断言。
* Each of the three changes pixels (gamma the frame, mipmap the distance, alpha test the cut-outs), so every
* section carries a **reverse** assertion.
* ——「把它设回默认,读数必须逐位回到原样」。
* Set it back to the default and the reading must return bit-for-bit.
*
* ## 为什么判据挑的是这些量(而不是"有没有那个接口")
* ## Why these particular measurements (and not "does the entry point exist")
*
* · **伽马**:不能只断言"变亮了" —— 整体乘 1.5 也叫变亮。
* · **gamma**: "it got brighter" is not enough - multiplying by 1.5 also brightens.
* 这里拿**实测的关闭值**套一遍 sRGB 分段曲线,要求落在 ±2 以内。
* The measured off-value is run through the sRGB piecewise curve and must land within +-2.
* 再断言**暗部抬升 > 亮部抬升**(凹曲线的签名,线性提亮做不到这一点)。
* On top of that the **dark lift must exceed the bright lift** (the signature of a concave curve,
* which a linear brighten cannot produce).
* · **alpha test**:必须是**镂空**(露出后面的东西),不是"物体变透明"。
* · **alpha test**: it must be a **cut-out** (what is behind shows through), not "the object went translucent".
* 所以前面那张方片 alpha=0、后面一张实心:关掉 cutoff 读到前面、打开读到后面。
* So a front quad at alpha 0 sits in front of a solid back quad: cutoff off reads the front, cutoff on
* reads the back.
* · **mipmap**:断言的是**缩小时的取值离散度下降**,不是"调了 generateMipmap"。
* · **mipmap**: the criterion is that the **spread collapses when minified**, not that "generateMipmap was called".
* 关着时小方片上是黑/白相间的点采样(离散度大),开着时是 mip 层给出的平均值(离散度小)。
* With mips off a tiny quad shows point-sampled black/white cells (large spread); with mips on it shows
* the averaged mip level (small spread).
* · **采样器**:U 方向跑到 `[1,2]` 的网格 + 一张横向渐变图 ⇒ 重复会看到整条渐变。
* · **sampler**: a grid whose U runs over [1,2] plus a horizontal gradient texture: repeat shows the whole ramp.
* 钳边会把整片糊成最后一列的颜色 —— 两者在同一张图上**一眼分得开**。
* Clamp smears the last column over everything - unmistakable on the same quad.
*
* ## 跑法
* ## How to run
* dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll \
* third_party/vml/Examples/c/3d/gamma_demo.c --screen 393x757
* 手机:命令行页敲 `vml run examples/c/3d/gamma_demo.c`
* Phone: on the command-line page type `vml run examples/c/3d/gamma_demo.c`
*
* 机器判读:逐行 `PASS …` / `FAIL …`,末行 `GAMMA_DEMO PASS|FAIL (fails=N)`。
* Machine-readable: one `PASS ...` / `FAIL ...` line each, ending with `GAMMA_DEMO PASS|FAIL (fails=N)`.
*/
#include <vml_ui.h>
#include <vml_3d.h>
#include <math.h> /* ⚠ 见 g3_all.c 的文件头:漏了它 sin/cos 静默返回垃圾,而程序不报错 */
/* See the g3_all.c header: without it sin/cos silently return garbage. */
#include <stdio.h>
/* ══ 记分 ══════════════════════════════════════════════════════════════════ */
/* == Scoring == */
int g_fails;
void ck(char* tag, int cond, int a, int b) {
if (cond) { printf("PASS %s %d %d\n", tag, a, b); }
else { g_fails = g_fails + 1; printf("FAIL %s %d %d\n", tag, a, b); }
}
int rch(int c) { return (c >> 16) & 0xFF; }
int gch(int c) { return (c >> 8) & 0xFF; }
int bch(int c) { return c & 0xFF; }
/* ══ 网格装配(pos3 + nrm3 + uv2 = 8 个 float32 = 32 字节/顶点,紧随其后是 int32 索引)════
* == Mesh assembly (pos3 + nrm3 + uv2 = 8 float32 = 32 bytes per vertex, then int32 indices) == */
float MB[256];
int g_vn;
int g_in;
void mb_vertex(int i, float px, float py, float pz,
float nx, float ny, float nz, float u, float v) {
int b = i * 8;
MB[b + 0] = px; MB[b + 1] = py; MB[b + 2] = pz;
MB[b + 3] = nx; MB[b + 4] = ny; MB[b + 5] = nz;
MB[b + 6] = u; MB[b + 7] = v;
}
void mb_index(int k, int v) { int* ip; ip = (int*)&MB[0]; ip[g_vn * 8 + k] = v; }
int mb_ptr(void) { return (int)&MB[0]; }
/* 一个正对 +Z 的方片(边长 2、中心在原点),**四角 uv 由参数给**。
* A quad facing +Z (side 2, centred at the origin); the four corner uvs come from the arguments.
* ⚠ uv 必须**四角互不相同** —— 全 0 的网格上"贴图没接上"与"贴了纯色图"读出来一模一样(见 tex_demo)。
* ⚠ The four corners must be **distinct** - on an all-zero grid a broken uv attribute reads exactly
* like a solid-colour texture (see tex_demo).
* ⚠ `u1` 可以**大于 1**:`#651` 的平铺模式判据就靠"uv 跑到 [1,2]"这一段(见文件头)。 */
/* `u1` may be **greater than 1**: the wrap-mode criteria in #651 rely on uv running over [1,2]. */
void gen_quad(float u0, float v0, float u1, float v1) {
mb_vertex(0, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, u0, v1);
mb_vertex(1, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, u1, v1);
mb_vertex(2, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, u1, v0);
mb_vertex(3, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, u0, v0);
g_vn = 4;
mb_index(0, 0); mb_index(1, 1); mb_index(2, 2);
mb_index(3, 0); mb_index(4, 2); mb_index(5, 3);
g_in = 6;
}
/* ══ 取样 ══════════════════════════════════════════════════════════════════ */
/* == Sampling == */
int sx(int sc) { return sc & 0xFFFF; }
int sy(int sc) { return (sc >> 16) & 0xFFFF; }
/* n×n 网格里的**红通道离散度**(max - min):min==max 表示这一片是均匀的。
* The **red-channel spread** (max - min) over an n-by-n grid: min == max means the patch is uniform.
* ⚠ 用**离散度**而不是方差:判据要的是"还有没有黑/白在跳",而方差会被一格极值拉爆。
* ⚠ Spread rather than variance: the question is "are black and white still alternating", and a
* variance would be dominated by a single extreme sample. */
int spread_red(int vp, int cx, int cy, int half, int step) {
int i; int j; int lo; int hi; int v;
lo = 999; hi = -1;
for (j = -half; j <= half; j = j + 1)
for (i = -half; i <= half; i = i + 1) {
v = rch(ui_3d_viewport_get_pixel(vp, cx + i * step, cy + j * step));
if (v < lo) lo = v;
if (v > hi) hi = v;
}
return hi - lo;
}
/* sRGB 规范的分段曲线(**与三端着色器里那段是同一个式子**)。
* The sRGB piecewise curve (**the very same expression as the one in all three shaders**).
* 判据拿它当期望值 ⇒ 只对得上"真的做了曲线变换"的,对不上"整体变亮"的。
* The criterion uses it as the expectation, so only a real curve transform matches, not a linear brighten. */
float srgb_curve(float x) {
if (x <= 0.0031308f) return 12.92f * x;
return 1.055f * pow(x, 1.0f / 2.4f) - 0.055f;
}
int main(void) {
int w; int h; int vp;
int mesh01; int mesh12;
int o_dark; int o_lite; int o_front; int o_back; int o_mip; int o_wrap;
int img_check; int img_grad;
int r; int i; int j; int sc; int px; int py;
int d0; int l0; int d1; int l1; int d2; int l2; int dback;
int exp_d; int exp_l;
int sp_nomip; int sp_mips; int sp_nomip2; int sp_big;
int halfpx;
int wx; int wy; int wc; int wl; int wc2; int wl2;
g_fails = 0;
ui_win_open_ex("3D 渲染能力示例", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
w = ui_scr_get_w_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
h = ui_scr_get_h_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
printf("SCR=%dx%d\n", w, h);
if (w < 64 || h < 64) { printf("GAMMA_DEMO FAIL (fails=99)\n"); return 99; }
/* ── ① 两张贴图:64×64 单像素棋盘(给 mipmap 用)、64×8 横向渐变(给平铺用)─────
* -- 1) Two textures: a 64x64 one-pixel checkerboard (for mipmap) and a 64x8 horizontal
* gradient (for wrap) --
* ⚠ 棋盘**必须是单像素格**:mip 每一级把 2×2 平均一次,单像素格在**第一级**就精确平均成 50% 灰。
* ⚠ The checkerboard must use **one-pixel cells**: each mip level averages 2x2, so one-pixel cells average
* to exactly 50% grey already at level 1.
* ⇒ 只要 LOD ≥ 1,采样值就是均匀灰(判据因此可以把阈值卡到个位数)。
* => Any LOD of 1 or more samples a uniform grey (hence the single-digit threshold).
* ⚠⚠ **尺寸不能太小**(实测踩到):小方片盖住 ≈22 像素,32×32 的图在那里的 LOD 只有 **0.55**。
* ⚠⚠ The texture must **not be too small** (measured): the small quad covers about 22 pixels, where a
* 32x32 texture only reaches LOD **0.55**.
* ⇒ 三线性会混进一半 level 0(满对比棋盘),离散度只从 233 掉到 **106**,看着像"mipmap 没生效"。
* => Trilinear then still blends in half of level 0 (the full-contrast checkerboard), so the spread only
* drops 233 -> **106**, which reads like "mipmap did not work".
* 64×64 把同一个方片的 LOD 抬到 **1.55** ⇒ 混的是 level 1 与 2,**两级都已经是精确的 50% 灰**。
* 64x64 lifts the same quad to LOD **1.55**, blending levels 1 and 2 - both already exact 50% grey. */
for (j = 0; j < 64; j = j + 1)
for (i = 0; i < 64; i = i + 1) {
int c = 0xFF000000;
if ((i + j) % 2 == 0) c = 0xFFFFFFFF;
ui_2d_rect(8 + i, 8 + j, 1, 1, c, 1, 0, 0);
}
/* 横向渐变:R 从 0 到 252、G 从 255 到 3 ⇒ **左绿右红**,与蓝色背景一眼分得开。 */
/* Horizontal gradient: R runs 0 -> 252 while G runs 255 -> 3, i.e. **green on the left, red on
* the right**, unmistakable against the blue background. */
/* ⚠⚠ **画在棋盘下方 16 像素处**(y=88),不能贴着它。
* ⚠⚠ **Drawn 16 pixels below the checkerboard** (y=88), never touching it.
* 实测踩到:棋盘画到 64×64 之后,原先贴在 y=60 的渐变**压住了棋盘最下面 8 行**。
* Measured: once the checkerboard grew to 64x64, the gradient at y=60 **covered its bottom 8 rows**.
* 于是抓下来的"棋盘"底部混进一条彩色条,mip 的层平均不再均匀。
* The captured "checkerboard" then had a coloured stripe along the bottom and the mip levels were no longer uniform.
* ⇒ 判据读到离散度 45 而不是 0(看着像"mipmap 没生效")。
* => The criterion read a spread of 45 instead of 0 (which looks like "mipmap did not work"). */
for (i = 0; i < 64; i = i + 1) {
int rr = i * 4;
int gg = 255 - i * 4;
ui_2d_rect(8 + i, 88, 1, 8, 0xFF000000 | (rr << 16) | (gg << 8), 1, 0, 0);
}
ui_2d_present();
img_check = ui_2d_get_image(8, 8, 64, 64);
img_grad = ui_2d_get_image(8, 88, 64, 8);
ck("checker texture handle", img_check > 0, img_check, 0);
ck("gradient texture handle", img_grad > 0, img_grad, 0);
/* ⚠ 抓完图**必须把 2D 层清成透明**:不清的话 2D 层盖在 3D 视口上面,屏幕一片黑(tex_demo 实测)。 */
/* ⚠ After grabbing, the 2D layer **must be cleared to transparent**, otherwise it covers the 3D
* viewport and the screen goes black (measured in tex_demo). */
ui_2d_clear(0);
ui_2d_present();
/* ── ② 视口 + 相机 + 六个物体 ───────────────────────────────────────────
* -- 2) Viewport + camera + six objects --
* 相机在 (0,0,3) 看向原点、竖直张角 60° ⇒ 距离 3 处**每世界单位 ≈ (h/2)/tan(30°)** 像素。
* The camera sits at (0,0,3) looking at the origin with a 60-degree **vertical** angle, so at
* distance 3 one world unit is about (h/2)/tan(30) pixels. */
vp = ui_3d_viewport_create(0, 0, w, h, 0);
ck("viewport -> handle", vp > 0, vp, 0);
ui_3d_viewport_set_camera(vp, 0.0f, 0.0f, 3.0f, 0.0f, 0.0f, 0.0f);
ui_3d_viewport_set_fov(vp, 60.0f, 0.1f, 100.0f);
ui_3d_viewport_set_bg(vp, 0xFF1010C0); /* 蓝底:渐变图里 B=0 ⇒ 判"这一格是不是物体"只看 B */
/* Blue background: the gradient has B=0, so "is this
* pixel on the object" is decided by B alone. */
ui_3d_viewport_set_light(vp, 0.0f, 0.0f, 1.0f, 0xFFFFFFFF, 0.35f);
gen_quad(0.0f, 0.0f, 1.0f, 1.0f);
mesh01 = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
ck("mesh uv[0,1] -> handle", mesh01 > 0, mesh01, 0);
gen_quad(1.0f, 0.0f, 2.0f, 1.0f);
mesh12 = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
ck("mesh uv[1,2] -> handle", mesh12 > 0, mesh12, 0);
/* 六个物体都开**自发光** ⇒ 片元直接输出基色/纹理原色,判据的期望值才是可算的。 */
/* All six objects are **emissive**, so the fragment outputs the base/texture colour directly and
* the expected values stay computable. */
o_dark = ui_3d_obj_create(vp, mesh01);
ui_3d_obj_set_color(o_dark, 0xFF404040);
ui_3d_obj_set_emissive(o_dark, 1);
ui_3d_obj_set_scale(o_dark, 0.25f);
ui_3d_obj_set_pos(o_dark, -0.6f, 1.35f, 0.0f);
o_lite = ui_3d_obj_create(vp, mesh01);
ui_3d_obj_set_color(o_lite, 0xFFC0C0C0);
ui_3d_obj_set_emissive(o_lite, 1);
ui_3d_obj_set_scale(o_lite, 0.25f);
ui_3d_obj_set_pos(o_lite, 0.6f, 1.35f, 0.0f);
o_front = ui_3d_obj_create(vp, mesh01);
ui_3d_obj_set_color(o_front, 0x00FFFFFF); /* **alpha = 0** ⇒ 关掉 cutoff 也照样画出来 */
/* **alpha = 0**: without a cutoff it still draws. */
ui_3d_obj_set_emissive(o_front, 1);
ui_3d_obj_set_scale(o_front, 0.30f);
ui_3d_obj_set_pos(o_front, 0.0f, 0.45f, 0.35f); /* 更靠近相机 ⇒ 挡住后面那张 */
/* Closer to the camera, so it occludes the back quad. */
o_back = ui_3d_obj_create(vp, mesh01);
ui_3d_obj_set_color(o_back, 0xFF20C040); /* 实心绿:镂空之后必须**看见它** */
/* Solid green: it must be **seen** through the hole. */
ui_3d_obj_set_emissive(o_back, 1);
ui_3d_obj_set_scale(o_back, 0.45f);
ui_3d_obj_set_pos(o_back, 0.0f, 0.45f, 0.0f);
o_wrap = ui_3d_obj_create(vp, mesh12);
ui_3d_obj_set_color(o_wrap, 0xFFFFFFFF); /* 白色是乘性 tint 的中性元 */
/* White is the neutral element of the tint. */
ui_3d_obj_set_emissive(o_wrap, 1);
ui_3d_obj_set_scale(o_wrap, 0.25f);
ui_3d_obj_set_pos(o_wrap, 0.0f, -1.35f, 0.0f);
r = ui_3d_obj_set_tex(o_wrap, img_grad);
ck("gradient texture mapped", r == 0, r, 0);
o_mip = ui_3d_obj_create(vp, mesh01);
ui_3d_obj_set_color(o_mip, 0xFFFFFFFF);
ui_3d_obj_set_emissive(o_mip, 1);
ui_3d_obj_set_scale(o_mip, 0.05f); /* 先小:**缩小**才会走到 mip 层 */
/* Small at first: only **minification** reaches the mip levels. */
ui_3d_obj_set_pos(o_mip, 0.0f, -0.45f, 0.0f);
r = ui_3d_obj_set_tex(o_mip, img_check);
ck("checker texture mapped", r == 0, r, 0);
/* 每个物体报出来的屏幕中心 —— 判据一律以它为中心取样(**不按屏幕比例猜**,见 tex_demo)。 */
/* Sample around the screen centre each object reports - **never guess from screen proportions**
* (see tex_demo). */
sc = ui_3d_obj_get_screen(o_dark); px = sx(sc); py = sy(sc);
sc = ui_3d_obj_get_screen(o_lite); printf("LITE=%d,%d\n", sx(sc), sy(sc));
sc = ui_3d_obj_get_screen(o_front); printf("FRONT=%d,%d\n", sx(sc), sy(sc));
sc = ui_3d_obj_get_screen(o_mip); printf("MIP=%d,%d\n", sx(sc), sy(sc));
sc = ui_3d_obj_get_screen(o_wrap); printf("WRAP=%d,%d\n", sx(sc), sy(sc));
(void)r; (void)j;
/* ══ ③ 伽马 / 色调映射(`#649`)═════════════════════════════════════════ */
/* == 3) Gamma / tone mapping (#649) == */
sc = ui_3d_obj_get_screen(o_dark); px = sx(sc); py = sy(sc);
ui_2d_present();
d0 = ui_3d_viewport_get_pixel(vp, px, py);
sc = ui_3d_obj_get_screen(o_lite);
l0 = ui_3d_viewport_get_pixel(vp, sx(sc), sy(sc));
printf("GAMMA_OFF dark=%d,%d,%d light=%d,%d,%d\n",
rch(d0), gch(d0), bch(d0), rch(l0), gch(l0), bch(l0));
ck("gamma off: dark quad is dark", rch(d0) > 40 && rch(d0) < 90, rch(d0), 64);
ck("gamma off: light quad is light", rch(l0) > 160 && rch(l0) < 220, rch(l0), 192);
r = ui_3d_viewport_set_gamma(vp, 1);
ck("set_gamma(1) rc", r == 0, r, 0);
ui_2d_present();
d1 = ui_3d_viewport_get_pixel(vp, px, py);
l1 = ui_3d_viewport_get_pixel(vp, sx(ui_3d_obj_get_screen(o_lite)), sy(ui_3d_obj_get_screen(o_lite)));
printf("GAMMA_SRGB dark=%d light=%d\n", rch(d1), rch(l1));
ck("sRGB changes both quads (not a no-op)", rch(d1) != rch(d0) && rch(l1) != rch(l0), rch(d1), rch(l1));
/* 期望值**由实测的关闭值现算**(不是写死 138/225):换了屏幕尺寸、换了驱动也照样成立。 */
/* The expectation is **computed from the measured off-value**, not hard-coded: it holds on any
* screen size and any driver. */
exp_d = (int)(srgb_curve((float)rch(d0) / 255.0f) * 255.0f + 0.5f);
exp_l = (int)(srgb_curve((float)rch(l0) / 255.0f) * 255.0f + 0.5f);
ck("sRGB matches the piecewise curve on the dark quad", rch(d1) >= exp_d - 2 && rch(d1) <= exp_d + 2, rch(d1), exp_d);
ck("sRGB matches the piecewise curve on the light quad", rch(l1) >= exp_l - 2 && rch(l1) <= exp_l + 2, rch(l1), exp_l);
/* ★ 这一条才是"真的做了曲线"的签名:**凹曲线上暗部抬得比亮部多**(线性提亮恰好相反)。 */
/* ★ This is the signature of a real curve: on a concave curve the **dark lift exceeds the bright
* lift**, while a linear brighten does exactly the opposite. */
ck("curve signature: dark lifts more than light",
(rch(d1) - rch(d0)) > (rch(l1) - rch(l0)), rch(d1) - rch(d0), rch(l1) - rch(l0));
/* 色调映射(Reinhard):**高光被压回去** —— 亮块的值必须降到只做 sRGB 之下。 */
/* Tone mapping (Reinhard): **highlights get pushed back down** - the bright quad must end up
* below the sRGB-only value. */
r = ui_3d_viewport_set_gamma(vp, 2);
ck("set_gamma(2) rc", r == 0, r, 0);
ui_2d_present();
l2 = ui_3d_viewport_get_pixel(vp, sx(ui_3d_obj_get_screen(o_lite)), sy(ui_3d_obj_get_screen(o_lite)));
d2 = ui_3d_viewport_get_pixel(vp, px, py);
printf("GAMMA_REINHARD dark=%d light=%d\n", rch(d2), rch(l2));
ck("Reinhard compresses the bright quad below sRGB-only", rch(l2) < rch(l1), rch(l2), rch(l1));
ck("Reinhard still applied a real transform", rch(l2) != rch(l0), rch(l2), rch(l0));
r = ui_3d_viewport_set_gamma(vp, 9);
ck("gamma mode out of range is rejected", r < 0, r, -2);
/* 反向:设回 0 ⇒ **逐位**回到最初那两个读数("关着时一个像素都不许变"这条硬保证)。 */
/* Reverse: back to 0 and the two readings must return **bit for bit** (the "off changes not one
* pixel" guarantee). */
r = ui_3d_viewport_set_gamma(vp, 0);
ui_2d_present();
dback = ui_3d_viewport_get_pixel(vp, px, py);
printf("GAMMA_BACK dark=%d\n", rch(dback));
ck("gamma off again -> dark quad is bit-identical", dback == d0, dback, d0);
/* ══ ④ 镂空 alpha test(`#650`)═════════════════════════════════════════ */
/* == 4) Alpha test (#650) == */
sc = ui_3d_obj_get_screen(o_front); px = sx(sc); py = sy(sc);
ui_2d_present();
wc = ui_3d_viewport_get_pixel(vp, px, py);
printf("ALPHA_OFF centre=%d,%d,%d\n", rch(wc), gch(wc), bch(wc));
ck("cutoff off: the front quad (alpha 0) is still drawn",
rch(wc) > 200 && gch(wc) > 200 && bch(wc) > 200, rch(wc), gch(wc));
r = ui_3d_obj_alpha_test(o_front, 0.5f);
ck("alpha_test rc", r == 0, r, 0);
ui_2d_present();
wl = ui_3d_viewport_get_pixel(vp, px, py);
printf("ALPHA_ON centre=%d,%d,%d\n", rch(wl), gch(wl), bch(wl));
/* ★ 镂空的签名:**读到的是后面那张绿方片** —— 不是"变半透明",是这一块像素真的没了。 */
/* ★ The cut-out signature: the reading is the **green quad behind** - not "went translucent", the
* pixels really are gone. */
ck("cutout reveals the quad behind", gch(wl) > 150 && rch(wl) < 80, gch(wl), rch(wl));
/* 反向之一:把前面那张的 alpha 设回 255 ⇒ 同一个 cutoff 下它**必须重新出现**(阈值没乱杀)。 */
/* Reverse 1: give the front quad alpha 255 and it must **come back** at the same cutoff (the
* threshold is not simply killing everything). */
/* ⚠ 比的是**三个颜色通道**、不是整包 32 位:alpha=0 与 alpha=255 的**帧缓冲 alpha 不同**
* ⚠ Compare the **three colour channels**, not the packed 32-bit word: alpha 0 and alpha 255 leave
* (0x00FFFFFF vs 0xFFFFFFF),拿整包比会假红 —— 而 RGB 相同恰恰说明"这一块又是那张方片了"。
* **different framebuffer alphas** (0x00FFFFFF vs 0xFFFFFFFF) so a packed compare goes falsely
* red - while equal RGB is exactly the proof that the front quad is back. */
ui_3d_obj_set_color(o_front, 0xFFFFFFFF);
ui_2d_present();
wc2 = ui_3d_viewport_get_pixel(vp, px, py);
ck("alpha 255 survives the same cutoff (front quad is back)",
rch(wc2) == rch(wc) && gch(wc2) == gch(wc) && bch(wc2) == bch(wc), bch(wc2), bch(wc));
ui_3d_obj_set_color(o_front, 0x00FFFFFF);
/* 反向之二:cutoff 设回 0 ⇒ 画面**逐位**回到打开之前那一帧。 */
/* Reverse 2: cutoff back to 0 and the picture returns **bit for bit**. */
r = ui_3d_obj_alpha_test(o_front, 0.0f);
ui_2d_present();
wl2 = ui_3d_viewport_get_pixel(vp, px, py);
ck("cutoff off again -> bit-identical", wl2 == wc, wl2, wc);
/* ══ ⑤ 采样器:平铺模式(`#651`)═══════════════════════════════════════ */
/* == 5) Sampler: wrap mode (#651) == */
/* 渐变图 + uv∈[1,2]:**重复**会看到整条渐变(左绿右红),**钳边**把最后一列糊满整片(全红)。 */
/* Gradient texture with uv in [1,2]: **repeat** shows the whole ramp (green left, red right) while
* **clamp** smears the last column across everything (all red). */
sc = ui_3d_obj_get_screen(o_wrap);
wx = sx(sc); wy = sy(sc);
halfpx = (int)((float)h * 0.5f / (3.0f * 0.57735f) * 0.25f); /* 方片半宽(像素) */
/* Half-width of the quad in pixels. */
printf("WRAP centre=%d,%d halfpx=%d\n", wx, wy, halfpx);
ui_2d_present();
wc = ui_3d_viewport_get_pixel(vp, wx, wy);
wl = ui_3d_viewport_get_pixel(vp, wx - halfpx * 7 / 10, wy);
printf("WRAP_REPEAT centre=%d,%d,%d left=%d,%d,%d\n", rch(wc), gch(wc), bch(wc), rch(wl), gch(wl), bch(wl));
/* 先证明取样点**真的落在物体上**(B 通道只在物体上是 0)—— 否则下面那条判据是"隔着背景比的"。 */
/* First prove the probe is **really on the object** (only there is the B channel 0); otherwise the
* next assertion compares background with background. */
ck("repeat: both probes are on the quad", bch(wc) < 64 && bch(wl) < 64, bch(wc), bch(wl));
ck("repeat: the ramp runs left-to-right", rch(wc) - rch(wl) > 60, rch(wc), rch(wl));
r = ui_3d_obj_sampler(o_wrap, 1, 1, 1, 1); /* wrapS/T = 1 ⇒ 钳到边缘 */
/* wrapS/T = 1 means clamp to edge */
ck("obj_sampler rc", r == 0, r, 0);
ui_2d_present();
wc2 = ui_3d_viewport_get_pixel(vp, wx, wy);
wl2 = ui_3d_viewport_get_pixel(vp, wx - halfpx * 7 / 10, wy);
printf("WRAP_CLAMP centre=%d,%d,%d left=%d,%d,%d\n", rch(wc2), gch(wc2), bch(wc2), rch(wl2), gch(wl2), bch(wl2));
ck("clamp: the quad becomes uniform", rch(wc2) - rch(wl2) < 12 && rch(wc2) - rch(wl2) > -12, rch(wc2), rch(wl2));
ck("clamp: the left probe is brighter than under repeat", rch(wl2) > rch(wl) + 60, rch(wl2), rch(wl));
r = ui_3d_obj_sampler(o_wrap, 0, 0, 1, 1); /* 设回默认 */
/* back to the defaults */
ui_2d_present();
ck("wrap back to repeat -> bit-identical", ui_3d_viewport_get_pixel(vp, wx, wy) == wc, ui_3d_viewport_get_pixel(vp, wx, wy), wc);
r = ui_3d_obj_sampler(o_wrap, 3, 0, 1, 1);
ck("wrap out of range is rejected", r < 0, r, -2);
r = ui_3d_obj_sampler(o_wrap, 0, 0, 7, 1);
ck("minFilter out of range is rejected", r < 0, r, -2);
/* ══ ⑥ mipmap(`#651`)════════════════════════════════════════════════ */
/* == 6) mipmap (#651) == */
/* 小方片(scale 0.05)盖住 ≈22 像素,而贴图是 64 格 ⇒ 每个屏幕像素盖住约 3 个纹素(LOD ≈ 1.55)。 */
/* The small quad (scale 0.05) covers about 22 pixels while the texture has 64 cells, so each screen
* pixel covers roughly 3 texels (LOD about 1.55). */
sc = ui_3d_obj_get_screen(o_mip);
px = sx(sc); py = sy(sc);
printf("MIP centre=%d,%d\n", px, py);
sp_nomip = spread_red(vp, px, py, 3, 2);
printf("MIP_NOMIP spread=%d\n", sp_nomip);
/* ★ 关着 mip 时**必须有离散度** —— 这一条同时是"贴图真的贴上了、而且高频"的取证。 */
/* ★ With mips off there **must** be a spread - which doubles as proof that the texture really is
* mapped and really is high frequency. */
ck("no mipmap: the minified checkerboard still alternates", sp_nomip >= 40, sp_nomip, 40);
r = ui_3d_obj_sampler(o_mip, 0, 0, 5, 1); /* minFilter = 5 ⇒ 三线性(要 mip) */
/* minFilter = 5 means trilinear (needs mips) */
ck("sampler(trilinear) rc", r == 0, r, 0);
sp_mips = spread_red(vp, px, py, 3, 2);
printf("MIP_MIPS spread=%d\n", sp_mips);
/* ★ 这一条才是"mipmap 生效":**缩小时的离散度塌下去**(点采样 → 层平均)。 */
/* ★ This is what "mipmap works" means: the **spread collapses when minified** (point sampling
* becomes a level average). */
ck("mipmap: the spread collapses when minified", sp_mips < sp_nomip && sp_mips <= 24, sp_mips, sp_nomip);
/* 反向:minFilter 设回 1(线性、无 mip)⇒ 离散度**必须原样回来**。 */
/* Reverse: minFilter back to 1 (linear, no mips) and the spread must **come back**. */
r = ui_3d_obj_sampler(o_mip, 0, 0, 1, 1);
ui_2d_present();
sp_nomip2 = spread_red(vp, px, py, 3, 2);
printf("MIP_NOMIP_AGAIN spread=%d\n", sp_nomip2);
ck("mipmap off again -> spread comes back", sp_nomip2 == sp_nomip, sp_nomip2, sp_nomip);
/* 放大时**不该**有 mip 参与:大方片上两种 minFilter 必须给出同一片像素("只在该变的地方变")。 */
/* Magnification must **not** involve mips: on the big quad both minFilters must give the same
* patch (change only where it should change). */
ui_3d_obj_set_scale(o_mip, 1.0f);
ui_2d_present();
sc = ui_3d_obj_get_screen(o_mip);
sp_big = spread_red(vp, sx(sc), sy(sc), 2, 1);
ui_3d_obj_sampler(o_mip, 0, 0, 5, 1);
ui_2d_present();
printf("MIP_BIG linear=%d trilinear=%d\n", sp_big, spread_red(vp, sx(sc), sy(sc), 2, 1));
ck("magnification is identical with and without mipmap",
sp_big == spread_red(vp, sx(sc), sy(sc), 2, 1), sp_big, spread_red(vp, sx(sc), sy(sc), 2, 1));
if (g_fails == 0) printf("GAMMA_DEMO PASS (fails=0)\n");
else printf("GAMMA_DEMO FAIL (fails=%d)\n", g_fails);
return g_fails;
}