c/3d/objload.c · 9082 字节 · 下载原文
/* objload.c —— **从外部 `.obj` 文件加载造型**(而不是在代码里手搓顶点)。
* objload.c — **load a shape from an external `.obj` file** (instead of hand-coding vertices).
*
* 用户 2026-10-06:「3D 接口能否像真正的 3D 游戏那样从外部文件加载设计文件,
* User, 2026-10-06: "Can the 3D interface load a design from an external file, the way a real 3D game does,
* 不可能复杂造型都要写代码去实现」。
* without having to implement every complex shape in code?"
*
* 在这之前,每一个造型都要在 C 里生成顶点数组(见同目录 `air3d.c` 的 `gen_sphere`)。
* Before this, every shape had to generate its vertex array in C (see `gen_sphere` in `air3d.c` next door).
* 现在造型是**数据**:Blender 里做好、导出 `.obj`,代码只管"怎么摆、怎么动"。
* Now the shape is **data**: model it in Blender, export `.obj`, and the code only handles "how to place it, how to move it".
*
* ## 两条写法约定(都是实测出来的,不是风格)
* ## Two conventions of writing this (both measured, not stylistic)
*
* ① **先量再开缓冲**:`ui_3d_mesh_load(path, 0, 0)` ⇒ **只量不建**,返回需要的字节数
* (1) **Measure before allocating the buffer**: `ui_3d_mesh_load(path, 0, 0)` => **measures only, builds nothing**; returns the needed byte count
* (照 `snprintf(NULL, 0, …)` 的惯例)。于是不必猜缓冲要开多大 ——
* (following the `snprintf(NULL, 0, ...)` convention). So you never have to guess how big the buffer should be --
* 猜小了会**报错**(返回负值),不会静默截断把模型削掉一半。
* guessing too small **reports an error** (negative return) rather than silently truncating and chopping half the model off.
* ② 缓冲是**静态 `int` 数组**:宿主只给了 `memmove`、没有 `malloc`
* (2) The buffer is a **static `int` array**: the host only provides `memmove`, not `malloc` --
* (见 `Lib/api/memory.json`),所以网格缓冲只能自己声明。
* (see `Lib/api/memory.json`) -- so the mesh buffer has to be declared by the program itself.
* 每顶点 8 个 float32 = **8 个 int 的宽度**,索引每个 1 个 int。
* Each vertex is 8 float32 = **the width of 8 ints**; each index is 1 int.
*
* ## 跑法
* ## How to run it
* 桌面:`dotnet <vmlcli.dll> Examples/c/3d/objload.c`(3D 后端目前只有 macOS 实现)
* Desktop: `dotnet <vmlcli.dll> Examples/c/3d/objload.c` (the 3D backend is currently macOS-only)
* 手机:命令行页敲 `vml run examples/c/3d/objload.c`
* Phone: on the command-line page type `vml run examples/c/3d/objload.c`
* (换模型就改下面的 `OBJ_FILE`,可试 `cube.obj` / `sphere.obj` / `torus.obj`)
* (to switch models, change `OBJ_FILE` below; try `cube.obj` / `sphere.obj` / `torus.obj`)
*/
#include <vml_ui.h>
#include <vml_3d.h>
#include <stdio.h>
/* 要加载的模型。⚠ 只有这三个是**随仓库的**(`scripts/make-obj-models.py` 生成、无版权问题)。 */
/* The model to load. ⚠ only these three **ship with the repo** (generated by `scripts/make-obj-models.py`, no copyright issue). */
#define OBJ_FILE "cube.obj"
/* 缓冲容量:20000 顶点 × 8 + 60000 索引 = 22 万个 int ≈ 880 KB。
* Buffer capacity: 20000 vertices x 8 + 60000 indices = 220000 ints ~ 880 KB.
* 超了就 `OBJ_TOO_BIG`(**报错,不静默截断**)—— 想装更大的模型就把这两个数调大。
* Overflow gives `OBJ_TOO_BIG` (**an error, not a silent truncation**) -- raise these two numbers to fit a bigger model. */
#define MAX_V 20000
#define MAX_I 60000
int MB[(MAX_V * 8) + MAX_I];
#define MB_BYTES ((MAX_V * 8 + MAX_I) * 4)
int mb_ptr(void) { return (int)&MB[0]; }
int main(void) {
int msg[8];
int vp; int sw; int sh; int mesh; int obj;
int need;
ui_win_open_ex("OBJ 加载 OBJLOAD", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
sw = ui_scr_get_w_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
sh = ui_scr_get_h_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
if (sw <= 0) sw = 640;
if (sh <= 0) sh = 640;
vp = ui_3d_viewport_create(0, 0, sw, sh, 0);
if (vp <= 0) {
/* ⚠ **明说**,不画一片黑让人猜(这条与 bench3d 同一个口径)*/
/* ⚠ **Say it out loud** instead of painting a black screen and letting people guess (same stance as bench3d). */
printf("NO_3D_DEVICE=1 vp=%d\n", vp);
while (ui_win_is_closed() == 0) { ui_msg_fetch_wait(100); }
return 0;
}
ui_3d_viewport_set_camera(vp, 0.0f, 2.2f, 4.4f, 0.0f, 0.0f, 0.0f);
ui_3d_viewport_set_fov(vp, 55.0f, 0.1f, 2000.0f);
ui_3d_viewport_fit(vp, 1.6f);
ui_3d_viewport_set_bg(vp, -16250870); /* 深空色 0xFF08120A 的有符号写法 */
/* deep-space color 0xFF08120A, in signed form */
ui_3d_viewport_set_light(vp, 0.45f, 1.0f, 0.35f, -1, 0.45f);
ui_3d_viewport_set_depth(vp, 1);
/* ── ① 先量:`buf_cap = 0` 只量不建 ───────────────────────────── */
/* ── (1) Measure first: `buf_cap = 0` measures without building ── */
/* ⚠ 两条路径都试:**手机**的 ResolvePath 以工作区为根(`examples/c/3d/…`),
/* ⚠ Try both paths: on **phone** ResolvePath is rooted at the workspace (`examples/c/3d/...`),
* **桌面**以源文件所在目录为根(`cube.obj` 就在旁边)。各端沙箱规则不同是既有事实,
* on **desktop** it is rooted at the source file's directory (`cube.obj` sits right beside it). Per-platform sandbox rules differ -- that is a fact,
* 这里如实试两条,不假装它们一样。
* so try both honestly instead of pretending they are the same. */
need = ui_3d_mesh_load("examples/c/3d/" OBJ_FILE, 0, 0);
if (need <= 0) need = ui_3d_mesh_load(OBJ_FILE, 0, 0);
printf("OBJ_MEASURE=%d (cap=%d)\n", need, MB_BYTES);
if (need <= 0) {
printf("OBJ_NOT_FOUND=1 need=%d\n", need);
while (ui_win_is_closed() == 0) { ui_msg_fetch_wait(100); }
return 0;
}
if (need > MB_BYTES) {
printf("OBJ_TOO_BIG=1 need=%d cap=%d\n", need, MB_BYTES);
while (ui_win_is_closed() == 0) { ui_msg_fetch_wait(100); }
return 0;
}
/* ── ② 真加载 ─────────────────────────────────────────────────── */
/* ── (2) Real load ─────────────────────────────────────────── */
mesh = ui_3d_mesh_load("examples/c/3d/" OBJ_FILE, mb_ptr(), MB_BYTES);
if (mesh <= 0) mesh = ui_3d_mesh_load(OBJ_FILE, mb_ptr(), MB_BYTES);
printf("OBJ_MESH=%d bytes=%d\n", mesh, need);
if (mesh <= 0) {
printf("OBJ_LOAD_FAIL=1 mesh=%d\n", mesh);
while (ui_win_is_closed() == 0) { ui_msg_fetch_wait(100); }
return 0;
}
obj = ui_3d_obj_create(vp, mesh);
ui_3d_obj_set_scale(obj, 1.0f);
ui_3d_obj_set_spin(obj, 0.0f, 40.0f, 0.0f); /* 让它转起来 —— 一眼看出是立体不是贴图 */
/* make it spin -- so you can tell at a glance it is 3D, not a texture */
printf("OBJ_READY=1 obj=%d\n", obj);
while (ui_win_is_closed() == 0) {
if (ui_key_is_down(VML_KEY_ESCAPE)) break;
/* ⚠⚠ **这一句不能省**:它把 2D 层清成**透明**(`Scene().Clear(0)`),3D 视口才透得出来。
/* ⚠⚠ **This line cannot be omitted**: it clears the 2D layer to **transparent** (`Scene().Clear(0)`) so the 3D viewport shows through.
* 少了它屏幕上**一片黑**、而 `OBJ_MEASURE/OBJ_MESH/OBJ_READY` 三行读数**全是对的**
* Without it the screen is **all black**, while the `OBJ_MEASURE/OBJ_MESH/OBJ_READY` readings are **all correct**
* —— 实测(2026-10-06 真机):加载全通、就是一个三角形都看不见。
* -- measured (2026-10-06 on a real device): the load fully succeeds, yet not one triangle is visible.
* 同目录每个 3D 例子每帧都有这一句(`bench3d`/`solar3d`/`tank3d`…),不是可选项。
* Every 3D example in this directory has this line on every frame (`bench3d`/`solar3d`/`tank3d`...); it is not optional. */
ui_2d_clear(0);
ui_2d_present();
/* ⚠ 这一句只是**等输入最多 40ms**;上屏节奏归宿主(自 2026-10-04 起)。 */
/* ⚠ This line only **waits for input for at most 40ms**; the on-screen pacing belongs to the host (since 2026-10-04). */
ui_msg_fetch_wait(40);
}
return 0;
}