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

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