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

c/3d/shadow_demo.c · 15772 字节 · 下载原文

/* shadow_demo.c —— **平面投影阴影**(`ui_3d_viewport_set_shadow` `#642`)专项示例 + 判据。
 * shadow_demo.c — **planar projected shadow** (`ui_3d_viewport_set_shadow` `#642`): example and criteria.
 *
 * 一片草地(y = 0 的平面)+ 一个悬在空中的球 + 太阳光 ⇒ 球在地上投下一个影子。
 * A lawn (the y = 0 plane), a ball floating in the air, and a sun overhead: the ball casts a shadow on the ground.
 *
 * ## 接收面固定 y = 0(这条限制要写进判据里)
 * ## The receiving plane is fixed at y = 0 (and that belongs in the criteria)
 *
 * 做法是把物体沿主光方向压到 y = 0 平面,生成一条半透明提交项、复用透明那条路 ⇒ 三端零新增依赖。
 * It is done by squashing the object onto the y = 0 plane along the main light and emitting a translucent
 * item that reuses the transparency path, so all three backends need nothing new.
 *
 * 代价是:只有一个接收面(地面抬高 / 台阶 / 墙面上没有影子)、没有自阴影、没有物体间遮挡、只按第 0 盏光投影。
 * The cost: one receiving plane only (nothing on raised ground, steps or walls), no self-shadowing, no
 * object-to-object occlusion, and only the **first** light is used.
 *
 * ## ⚠ 一条会静默失效的路
 * ## ⚠ One silent failure path
 *
 * 主光的 **y 分量必须 > 0.05**。光写成水平或朝下(`dy <= 0.05`)时**一个影子都不生成,而且不报错**。
 * The main light's **y component must be > 0.05**; a horizontal or downward light (`dy <= 0.05`) generates
 * **no shadow at all and reports nothing**.
 *
 * ⇒ 本文件最后有一条**见证判据**把这条限制钉住:把光放平,影子的读数必须**回到没开影子时一样**。
 * => The last criterion here freezes that limitation: with a horizontal light the reading must **return to
 *   exactly what it was with shadows off**.
 *
 * ## 跑法
 * ## How to run
 *   dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll \
 *          third_party/vml/Examples/c/3d/shadow_demo.c --screen 393x757
 *
 * 机器判读:逐行 `PASS …` / `FAIL …`,末行 `SHADOW_DEMO PASS|FAIL (fails=N)`。
 * Machine-readable: one `PASS ...` / `FAIL ...` line each, ending with `SHADOW_DEMO PASS|FAIL (fails=N)`.
 */

#include <vml_ui.h>
#include <vml_3d.h>
#include <math.h>
#include <stdio.h>
#include <file.h>

/* ══ 记分 ══════════════════════════════════════════════════════════════════ */
/* == Scoring == */

int g_fails;

/* 判据除了打 stdout,还写一份**结果文件**。
 * Besides stdout, every criterion is also written to a **result file**.
 *
 * ⚠ 手机端跑开窗程序时命令行页会被绘图页顶掉,驱动"看到窗口就算跑完" ⇒ 只抓屏会漏掉判据正文。
 * ⚠ On a phone a window-opening program hides the command-line page and the harness counts "window appeared" as done, so scraping the screen misses the criteria.
 *
 * 结果文件落在 cwd(手机上是工作区根),`adb pull` 直接取得到。
 * The file lands in cwd (the workspace root on a phone) and is directly pullable with `adb pull`. */

char RLINE[192];

/* ⚠ 句柄 **0 是合法值**(实测 `fopen` 成功返回 0)⇒ 判"开没开成"必须用**负值**。
 * ⚠ Handle **0 is valid** (measured: a successful `fopen` returns 0), so "did it open" must test for a **negative** value. */

int g_rpt = -2;   /* -2 = 还没试过,>= 0 = 已打开,< 0 = 开不了 */

void rpt_line(char* s) {
    int n;
    if (g_rpt == 0 - 2) g_rpt = fopen("_3d_shadow_result.txt", 1);
    if (g_rpt < 0) return;
    n = 0;
    while (s[n] != 0) n = n + 1;
    fwrite(g_rpt, s, n);
}

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); }
    sprintf(RLINE, "%s %s %d %d\n", cond ? "PASS" : "FAIL", tag, a, b);
    rpt_line(RLINE);
}

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

/* ══ 网格装配(pos3 + nrm3 + uv2 = 8 个 float32,索引 int32 紧随其后)══════════ */
/* == Mesh assembly (pos3 + nrm3 + uv2 = 8 float32, then int32 indices) == */

float MB[8192];
int g_vn;
int g_in;

void mb_vertex_uv(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]; }

/* y = 0 的草地:法线恒 +Y(**按面给常量法线**,不要拿归一化位置当法线)。
 * The lawn at y = 0 with a constant +Y normal (use a per-face constant, never the normalised position). */

void gen_ground(float half, int segs) {
    int i; int j; int v; int k;
    v = 0;
    for (j = 0; j <= segs; j = j + 1) {
        for (i = 0; i <= segs; i = i + 1) {
            float fx = 0 - half + 2.0f * half * i / segs;
            float fz = 0 - half + 2.0f * half * j / segs;
            mb_vertex_uv(v, fx, 0.0f, fz, 0.0f, 1.0f, 0.0f, 4.0f * i / segs, 4.0f * j / segs);
            v = v + 1;
        }
    }
    g_vn = v;
    k = 0;
    for (j = 0; j < segs; j = j + 1) {
        for (i = 0; i < segs; i = i + 1) {
            int a = j * (segs + 1) + i;
            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;
}

/* 单位球:法线就是归一化的位置。
 * A unit sphere: the normal is the normalised position. */

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_uv(v, nx * r, ny * r, nz * r, nx, ny, nz,
                         2.0f * j / segs, 1.0f - 2.0f * i / rings);
            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;
}

int main(void) {
    int w; int h; int vp; int m_ground; int m_ball;
    int o_ground; int o_ball;
    int r; int c_off; int c_on; int c_back; int c_tilt; int c_flat;
    int lang;

    g_fails = 0;
    lang = ui_scr_get_language();

    ui_win_open_ex(lang == 0 ? "3D 平面阴影示例" : "3D Shadow Demo",
                   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("SHADOW_DEMO FAIL (fails=99)\n"); return 99; }

    /* ⚠ 单独开一个视口:影子落在**世界原点**(相机正看着它),而主场景里原点往往被别的物体占着。
     * ⚠ A dedicated viewport: the shadow lands at the **world origin**, which the camera looks straight at.
     *   In a busy scene the origin is usually occupied by something else, and the reading gets clobbered. */

    /* ⚠ 视口按**实际可用区**开:别写死 512×512 —— 手机屏宽只有 ~393,写死会被裁,而判据读的正是视口中心那个像素。
     * ⚠ Size the viewport to the **actual usable area**: never hard-code 512x512, since a phone is only ~393 wide, a clipped viewport moves the centre pixel out of view, and that centre pixel is what we read. */
    vp = ui_3d_viewport_create(0, 0, w, h, 0);
    ck("viewport -> handle", vp > 0, vp, 0);

    /* 相机在高处俯视原点。
     * A camera high above, looking down at the origin. */

    ui_3d_viewport_set_camera(vp, 0.0f, 320.0f, 640.0f, 0.0f, 0.0f, 0.0f);
    ui_3d_viewport_set_fov(vp, 60.0f, 1.0f, 8000.0f);
    ui_3d_viewport_set_bg(vp, argb(120, 150, 190));

    /* ⚠ 主光**竖直向上**(`dy = 1`)—— 影子才落在正下方、正好是画面中心那个像素。
     * ⚠ The main light points **straight up** (`dy = 1`), so the shadow falls directly below the ball,
     *   which is exactly the centre pixel of the view. */

    r = ui_3d_viewport_set_light(vp, 0.0f, 1.0f, 0.0f, argb(255, 250, 235), 0.45f);
    ck("set_light(up) rc", r == 0, r, 0);

    /* ⚠⚠ 3D 视口是**透过 2D 层**看的 ⇒ 2D 层必须是透明的,否则窗口开了也是一片黑。
     * ⚠⚠ The 3D viewport shows **through** the 2D layer, so that layer must be transparent or the window opens black. */

    ui_2d_clear(0);

    gen_ground(400.0f, 8);
    m_ground = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    o_ground = ui_3d_obj_create(vp, m_ground);
    ui_3d_obj_set_color(o_ground, argb(90, 140, 80));
    ck("ground object -> handle", o_ground > 0, o_ground, 0);

    gen_sphere(16, 24, 30.0f);
    m_ball = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    o_ball = ui_3d_obj_create(vp, m_ball);
    ui_3d_obj_set_pos(o_ball, 0.0f, 130.0f, 0.0f);
    ui_3d_obj_set_color(o_ball, argb(230, 140, 60));
    ui_3d_obj_set_material(o_ball, argb(255, 235, 200), 48.0f);
    ck("ball object -> handle", o_ball > 0, o_ball, 0);

    /* ── ① 开影子之前:地面中心是**没有**暗斑的 ────────────────────────────── */
    /* ① Before shadows: the centre of the ground carries **no** dark patch */
    ui_2d_present();
    c_off = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);
    printf("CENTRE before shadow lum=%d\n", lum(c_off));
    ck("centre is lit ground before shadow", lum(c_off) > 120, lum(c_off), 0);

    /* ── ② 开影子 ⇒ 球正下方那块地面**变暗** ──────────────────────────────── */
    /* ② Turn shadows on => the ground right under the ball **darkens** */
    r = ui_3d_viewport_set_shadow(vp, 1);
    ck("set_shadow(1) rc", r == 0, r, 0);
    ui_2d_present();
    c_on = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);
    printf("CENTRE shadowed lum=%d\n", lum(c_on));

    /* ⚠ 这一条才是能响的那条:把 set_shadow 实现成"什么都不做"⇒ 读数与基线相同 ⇒ 红。
     * ⚠ This is the loud one: a no-op set_shadow leaves the reading equal to the baseline and turns this red. */

    ck("shadow darkens the ground under the ball", lum(c_on) < lum(c_off), lum(c_on), lum(c_off));

    /* ── ③ 反证:关掉影子 ⇒ **逐位**回到基线 ──────────────────────────────── */
    /* ③ Reverse: switch shadows off and it returns **bit-identically** to the baseline */
    r = ui_3d_viewport_set_shadow(vp, 0);
    ck("set_shadow(0) rc", r == 0, r, 0);
    ui_2d_present();
    c_back = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);
    ck("shadow off -> identical to baseline", c_back == c_off, c_back, c_off);

    /* ── ④ 影子跟着**光的方向**走(不是原地画一块固定的暗斑) ───────────────── */
    /* ④ The shadow follows the **light direction** (it is not a fixed blob in place) */

    /* 把主光斜过来 ⇒ 影子挪到 +X 一侧 ⇒ 原来那个中心像素必须**亮回去**。
     * Tilt the main light => the shadow moves to the +X side => the old centre pixel must **brighten back**. */

    ui_3d_viewport_set_shadow(vp, 1);
    r = ui_3d_viewport_set_light(vp, 0.6f, 0.8f, 0.0f, argb(255, 250, 235), 0.45f);
    ck("tilt light rc", r == 0, r, 0);
    ui_2d_present();
    c_tilt = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);
    printf("CENTRE tilted lum=%d (shadow at %d)\n", lum(c_tilt), lum(c_on));
    ck("shadow moved away with the light", lum(c_tilt) > lum(c_on), lum(c_tilt), lum(c_on));

    /* ── ⑤ 见证判据:主光放平(dy = 0)⇒ 影子**一个都不生成** ───────────────── */
    /* ⑤ Witness: a horizontal main light (dy = 0) generates **no shadow at all** */

    /* ⚠ 这是**已知限制**(主光 y 分量必须 > 0.05,否则整条路静默不走)。
     * ⚠ This is a **known limitation**: the main light's y must exceed 0.05 or the whole path silently no-ops. */

    /* ⚠⚠ 必须**在同一套光照下**比 —— 光从竖直改成水平**本身**就会改明暗。
     * ⚠⚠ Compare under **one and the same light** - turning it from vertical to horizontal changes the shading by itself. */

    /* `abs(dot(n, L))` 在 n=(0,1,0) 时从 1 掉到 0 ⇒ 只比"光不同"也会读出差异,那是明暗不是影子。
     * With n=(0,1,0) the term `abs(dot(n, L))` drops from 1 to 0, so comparing two lights measures shading, not shadow. */

    /* 从前这里拿「竖光 + 关影子」比「平光 + 开影子」⇒ 判据**必红**,而红的是明暗差(实测 2026-10-08)。
     * This used to compare "vertical light, shadows off" with "flat light, shadows on", so it could never pass. */

    /* 正解:光固定成水平,只切影子开关 —— 两次逐位相同才说明"平光一个影子都不生成"。
     * The fix: hold the light horizontal and toggle only the shadow flag; bit-identical readings pin "a flat light casts nothing". */

    ui_3d_viewport_set_light(vp, 1.0f, 0.0f, 0.0f, argb(255, 250, 235), 0.45f);
    ui_3d_viewport_set_shadow(vp, 0);
    ui_2d_present();
    c_off = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);

    ui_3d_viewport_set_shadow(vp, 1);
    ui_2d_present();
    c_flat = ui_3d_viewport_get_pixel(vp, w / 2, h / 2);
    printf("FLATLIGHT lum=%d baseline lum=%d\n", lum(c_flat), lum(c_off));
    ck("flat light: no shadow at all (known limit)", c_flat == c_off, c_flat, c_off);

    /* ── ⑥ 错误路径 ──────────────────────────────────────────────────────── */
    /* ⑥ Error paths */
    r = ui_3d_viewport_set_shadow(9999, 1);
    ck("bad viewport rejected", r < 0, r, 0);

    /* 收尾:恢复成"竖直光 + 开影子"的观感,供 --frames 出图。
     * Final: restore the upright light with shadows on so the exported frame shows the feature. */

    ui_3d_viewport_set_light(vp, 0.0f, 1.0f, 0.0f, argb(255, 250, 235), 0.45f);
    ui_3d_viewport_set_shadow(vp, 1);
    ui_2d_present();

    sprintf(RLINE, "SHADOW_DEMO %s (fails=%d)\n", g_fails == 0 ? "PASS" : "FAIL", g_fails);
    rpt_line(RLINE);
    printf("SHADOW_DEMO %s (fails=%d)\n", g_fails == 0 ? "PASS" : "FAIL", g_fails);
    if (g_rpt >= 0) fclose(g_rpt);
    return g_fails;
}