← 都来码 / Dolaima← 例程索引c/3d/light_demo.c原文

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;
}