c/3d/light_demo.c · 13307 字节 · 下载原文
/* light_demo.c —— **光照 / 材质 / 雾**的观感示例(2026-10-08 加的三个号)。
* light_demo.c — a showcase for light / material / fog (the three numbers added 2026-10-08).
*
* ## 这个程序和 `g3_all.c` 的分工
* ## How this differs from `g3_all.c`
*
* `g3_all.c` 是**逐号验收**(每个号一条能响的判据 + 反证,机器读 PASS/FAIL)。
* `g3_all.c` is **per-number acceptance** (one fir-able criterion per number, machine-readable).
* 这一份是**给人看的**:同一个球面,三种材质并排站开,两盏不同色温的光,远处一层雾。
* This one is **for a human**: three materials side by side, two lights of different colour
* temperature, and a layer of fog in the distance.
*
* 出图(桌面,秒级):
* Run it headless and write a PNG:
* dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll \
* third_party/vml/Examples/c/3d/light_demo.c --screen 393x757 --frame /tmp/light.png
*
* ⚠ **它是静态场景**(没有 spin / orbit / 时钟):`--frame` 出图**可复现**,
* 可以拿两次运行的 sha256 比对做回归(`PngEncoder` 里没有时间戳,出图是确定性的)。
* ⚠ It is a **static scene** so `--frame` is reproducible and can be used as a regression
* baseline (two runs must produce byte-identical PNGs).
*
* ## 三个球为什么长得不一样
* ## Why the three balls look different
*
* 高光是 **Blinn-Phong**(`ui_3d_obj_set_material`,`#638`):亮的**位置与形状**由
* `specular`(颜色)与 `shininess`(指数)两个参数定 ——
* Specular is **Blinn-Phong**: where and how tight the highlight is comes from
* `specular` (colour) and `shininess` (exponent):
*
* · **金属**:`specular` 跟基色同调(金色球给金色高光)、指数中等 ⇒ 亮斑大而"实"
* · **塑料**:`specular` 近白、指数**高** ⇒ 小而锐的光点(塑料的典型观感)
* · **磨砂/粉笔**:`specular = 0`(**默认**)⇒ 只有漫反射,一点高光都没有
*
* ⚠ 高光是**乘性**的:`specular × 光色 × pow(...)` ⇒ 没有光的方向上它也不会亮。
* 所以"给材质"这件事必须与"摆光源"一起做,单给材质看不出效果。
*/
#include <vml_ui.h>
#include <vml_3d.h>
#include <math.h> /* ⚠ 见 g3_all.c 文件头:漏了它 sin/cos 静默返回垃圾,程序不报错 */
#include <stdio.h>
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; }
int lum(int c) { return rch(c) + gch(c) + bch(c); }
int argb(int r, int g, int b) { return -16777216 + r * 65536 + g * 256 + b; }
/* ══ 网格装配(与 g3_all.c 同一套约定:pos3 + nrm3 + uv2 = 8 个 float32)════
*
* ⚠⚠ **这个数组要装得下"顶点数 × 8 + 索引数"个 float** —— 算错了**不会报错**:
* 越界写会踩掉隔壁的全局变量,症状是"网格建出来了、`mesh_create` 返回句柄、
* 但**球一个像素都不画**"(实测:`gen_sphere(16,24,·)` 要 425×8 + 2304 = 5704,
* 而这里原本写的 4400 ⇒ 球整个消失、画面只剩清屏色)。
* ⚠ 算一遍再定大小:顶点 (rings+1)×(segs+1)、索引 rings×segs×6。 */
float MB[8192];
int g_vn;
int g_in;
void mb_vertex(int i, float px, float py, float pz, float nx, float ny, float nz) {
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] = 0.0f; MB[b + 7] = 0.0f; /* 本示例不贴图,uv 留 0 */
}
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]; }
/* 单位球:法线就是归一化的位置。 */
void gen_sphere(int rings, int segs, float r) {
int i; int j; int k; int v;
v = 0;
for (i = 0; i <= rings; i = i + 1) {
float phi = 3.14159265f * i / rings;
float sp = sin(phi);
float cp = cos(phi);
for (j = 0; j <= segs; j = j + 1) {
float th = 6.28318531f * j / segs;
float nx = sp * cos(th);
float ny = cp;
float nz = sp * sin(th);
mb_vertex(v, nx * r, ny * r, nz * r, nx, ny, nz);
v = v + 1;
}
}
g_vn = v;
k = 0;
for (i = 0; i < rings; i = i + 1) {
for (j = 0; j < segs; j = j + 1) {
int a = i * (segs + 1) + j;
int b = a + segs + 1;
mb_index(k, a); k = k + 1;
mb_index(k, b); k = k + 1;
mb_index(k, a + 1); k = k + 1;
mb_index(k, a + 1); k = k + 1;
mb_index(k, b); k = k + 1;
mb_index(k, b + 1); k = k + 1;
}
}
g_in = k;
}
/* 数"明显亮"的采样点(高光斑的大小 = 亮点个数)。
* ⚠ 步长 3:`ui_3d_viewport_get_pixel` 是**逐点一趟线程往返**(见 E137),别密集采样。 */
int count_bright(int vp, int cx, int cy, int r, int thr) {
int i; int j; int n; n = 0;
for (j = -r; j <= r; j = j + 3)
for (i = -r; i <= r; i = i + 3) {
int c = ui_3d_viewport_get_pixel(vp, cx + i, cy + j);
if (lum(c) > thr) n = n + 1;
}
return n;
}
int main(void) {
int w; int h; int vp; int mesh;
int o_metal; int o_plastic; int o_matte;
int r; int sc; int px; int py;
int n_metal; int n_plastic; int n_matte;
int b_metal; int b_plastic; int b_matte;
g_fails = 0;
ui_win_open_ex("光照 / 材质 / 雾", 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("LIGHT_DEMO FAIL (fails=99)\n"); return 99; }
vp = ui_3d_viewport_create(0, 0, w, h, 0);
ck("viewport -> handle", vp > 0, vp, 0);
/* 相机退到 (0,0,14) 看向原点;三个球并排在 x = -2.6 / 0 / +2.6。
* ⚠ **退足够远是必须的**:`set_fov` 是**竖直**张角,而竖屏的宽高比只有 ~0.52
* ⇒ 水平半宽 ≈ 竖直半宽 × 0.52。第一版写 z=9 + x=±3.2(水平半宽才 2.44)
* ⇒ **左右两个球整个在屏幕外**,而程序照样「三个物体建好了」——
* 自检把同一个背景像素读了三次(实测三行读数一模一样)。 */
ui_3d_viewport_set_camera(vp, 0.0f, 0.0f, 14.0f, 0.0f, 0.0f, 0.0f);
ui_3d_viewport_set_fov(vp, 55.0f, 1.0f, 200.0f);
ui_3d_viewport_set_bg(vp, 0xFF10141C);
/* ── 雾(`#636`):远处泛蓝 —— 让"远近"看得出来 ───────────────────────────── */
/* 相机在 z=14、球在 z=0 ⇒ 距离约 14;雾从 10 起、到 60 满 ⇒ 球吃一点雾、背景吃满 */
r = ui_3d_viewport_set_fog(vp, argb(30, 40, 70), 10.0f, 60.0f);
ck("set_fog rc", r == 0, r, 0);
/* ── 两盏光(`#612` + `#637`):一暖一冷 —— 单光源是"粉笔感"的最大来源 ───────── */
/* 第 0 盏(= 原来的 #612):**正对相机**的暖白主光。
* ⚠ 方向选 +Z 是有意的:视线也是 +Z ⇒ `H = normalize(L+V) ≈ +Z` ⇒ 高光斑**落在球心那个像素**上,
* 自检只要读那一个点就够(不必满屏找最亮处在哪儿)。 */
ui_3d_viewport_set_light(vp, 0.0f, 0.0f, 1.0f, argb(255, 235, 210), 0.12f);
/* 第 1 盏(`#637` 新加的):左后方的**冷蓝**补光 —— 双面光照是 abs(dot) ⇒ 背面也照亮 */
r = ui_3d_viewport_set_light_at(vp, 1, 0 - 0.7f, 0.2f, 0 - 0.6f, argb(90, 130, 255), 0.10f);
ck("light_at(1) rc", r == 0, r, 0);
/* 第 2 盏:正下方的微光,给球底一点环境感 */
r = ui_3d_viewport_set_light_at(vp, 2, 0.0f, 0 - 1.0f, 0.0f, argb(60, 60, 70), 0.06f);
ck("light_at(2) rc", r == 0, r, 0);
gen_sphere(16, 24, 0.8f);
mesh = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
ck("mesh_create -> handle", mesh > 0, mesh, 0);
o_metal = ui_3d_obj_create(vp, mesh);
o_plastic = ui_3d_obj_create(vp, mesh);
o_matte = ui_3d_obj_create(vp, mesh);
ck("three objects created", o_metal > 0 && o_plastic > 0 && o_matte > 0, o_metal, o_matte);
/* 三个球并排:金 / 青 / 灰。间距 1.8 > 直径 1.6 ⇒ 不重叠;
* ⚠ **别让球贴着屏幕边缘**:贴边时球心的投影点会落在球体的边界附近,
* 自检读到的就是背景色(第一版 x=±3.2 时正是如此 —— 坐标报得没错,但那个点不在球上)。 */
ui_3d_obj_set_pos(o_metal, 0 - 1.8f, 0.0f, 0.0f);
ui_3d_obj_set_pos(o_plastic, 0.0f, 0.0f, 0.0f);
ui_3d_obj_set_pos(o_matte, 1.8f, 0.0f, 0.0f);
ui_3d_obj_set_color(o_metal, argb(190, 150, 60)); /* 金 */
ui_3d_obj_set_color(o_plastic, argb(60, 160, 170)); /* 青 */
ui_3d_obj_set_color(o_matte, argb(150, 150, 155)); /* 灰 */
/* ⚠ 自证:三个球**都真的落在屏幕里**。
* 这是本文件第一版翻的车:`set_fov` 是**竖直**张角,而竖屏的宽高比只有 ~0.52
* ⇒ 相机太近(z=9)时左右两个球**整个在屏幕外**,而程序照样报"三个物体建好了" ——
* 自检把**同一个背景像素**读了三次(实测三行读数一模一样:`CENTRE … 64,64,764`)。
* ⚠ 这类"物体在画面外"的故障**不报错、也不崩**,只能靠"它在不在屏内"这条直接量。 */
sc = ui_3d_obj_get_screen(o_metal); px = sc & 0xFFFF;
ck("left ball on screen", px > 0 && px < w, px, w);
sc = ui_3d_obj_get_screen(o_matte); px = sc & 0xFFFF;
ck("right ball on screen", px > 0 && px < w, px, w);
/* ── 三种材质(`#638`)+ **相对判据**的自检 ────────────────────────────────
* ⚠ 为什么不用"最亮点个数 / 绝对亮度阈值"这类判据:三盏光叠起来之后**磨砂球本身就挺亮**
* (实测整片采样区都超阈值 ⇒ 441/441 全亮、而真正的高光球反而 0 —— 判据整个反了过来)。
* ⇒ 改成**相对**的:"设材质前后,球心那一点有没有变亮",与光照量级无关。 */
ui_2d_present();
sc = ui_3d_obj_get_screen(o_metal); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
b_metal = lum(ui_3d_viewport_get_pixel(vp, px, py));
sc = ui_3d_obj_get_screen(o_plastic); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
b_plastic = lum(ui_3d_viewport_get_pixel(vp, px, py));
sc = ui_3d_obj_get_screen(o_matte); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
b_matte = lum(ui_3d_viewport_get_pixel(vp, px, py));
/* 金属:高光**跟基色同调**(金色高光)、指数中等 ⇒ 大而实的亮斑 */
ui_3d_obj_set_material(o_metal, argb(255, 220, 120), 24.0f);
/* 塑料:高光**近白**、指数**高** ⇒ 小而锐的光点 */
ui_3d_obj_set_material(o_plastic, argb(255, 255, 255), 160.0f);
/* 磨砂:`specular = 0`(**默认值**)⇒ 一点高光都没有,只有漫反射 */
ui_3d_obj_set_material(o_matte, 0, 32.0f);
printf("MATERIAL metal=24 plastic=160 matte=0\n");
ui_2d_present();
sc = ui_3d_obj_get_screen(o_metal); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
n_metal = lum(ui_3d_viewport_get_pixel(vp, px, py));
sc = ui_3d_obj_get_screen(o_plastic); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
n_plastic = lum(ui_3d_viewport_get_pixel(vp, px, py));
sc = ui_3d_obj_get_screen(o_matte); px = sc & 0xFFFF; py = (sc >> 16) & 0xFFFF;
n_matte = lum(ui_3d_viewport_get_pixel(vp, px, py));
printf("CENTRE before=%d,%d,%d after=%d,%d,%d\n",
b_metal, b_plastic, b_matte, n_metal, n_plastic, n_matte);
/* 金属与塑料的**球心变亮了**(高光正落在那里)。
* ⚠ 这两条是能响的:把 `set_material` 实现成"什么都不做"⇒ 相等 ⇒ 红。 */
ck("metal highlight brightens centre", n_metal > b_metal, n_metal, b_metal);
ck("plastic highlight brightens centre", n_plastic > b_plastic, n_plastic, b_plastic);
/* 磨砂**逐位不变**(`specular = 0` ⇒ 片元那一项恒为 0)—— "默认无高光"那颗钉子。
* ⚠ 反证方向:把"无高光"实现成"默认给个白高光"⇒ 它立刻 > 0。 */
ck("matte centre unchanged (specular=0)", n_matte == b_matte, n_matte, b_matte);
/* 两个高光球的中心亮度**不同**(指数 24 vs 160 ⇒ 一个糊一个锐;基色也不同) */
ck("metal and plastic differ", n_metal != n_plastic, n_metal, n_plastic);
printf("LIGHT_DEMO %s (fails=%d)\n", g_fails == 0 ? "PASS" : "FAIL", g_fails);
return g_fails;
}