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