c/3d/glbview.c · 28704 字节 · 下载原文
/* glbview.c —— **glb / gltf 查看器**。
* glbview.c - a **glb / gltf viewer**.
*
* 用户口径(2026-10-08):「接口实现完成后,先做个 glb 查看器,然后再做个游戏把 glb 用上」。
* User's call (2026-10-08): "once the interfaces are done, first make a glb viewer, then make a game that uses glb".
*
* ══ 怎么用 ══════════════════════════════════════════════════════════════════
* 手机:命令行页敲 `vml run examples/c/3d/glbview.c`
* 桌面:`dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/c/3d/glbview.c --screen 393x757`
*
* 单指拖 = 转视角 两指开合 = 拉近拉远
* A = 自动旋转 B = 换一个模型(候选里**装得上**的那些之间循环)
* X = 输出变换循环 Y = 阴影开关(`#642` 平面投影阴影)
* SELECT = PBR 开关(**默认开**)—— 金属度-粗糙度贴图(`#661`)只在它开着时才起作用
* ↑/↓ = 环境强度(金属反射的亮度) R = 复位视角 Esc = 退出
*
* ⚠ 手机上这几个键来自**自带手柄**(方向键 + X/Y/A/B → Win32 虚拟键),程序里的键盘分支直接认。
*
* ══ 它**不**做什么(如实记下,别指望)════════════════════════════════════════
* · **不列目录**:`vml_readdir` 在本仓宿主上**没有实现**(实测返回 -8,C# 侧零命中)⇒ 候选是
* 写死的一串(见 CANDIDATES)。想看你自己的模型,照那几条路径放,或改那张表。
* · **不播动画**:glTF 的 `animations` 还没解析,带骨骼的模型显示的是**绑定姿势**。
* ⚠ 蒙皮接口(`#656`–`#660`)已经能用,但**装载器还没解析 `skins`** ⇒ 这里也不解。
* · 不支持 Draco / KTX2(装载器的已知缺口)。
*
* ══ 为什么值得单独立一个程序 ═════════════════════════════════════════════════
* `#643`(glTF 2.0 静态装载)落地时的验收靠三个大模型 + 逐条跟浏览器对表,**重**。
* 这个查看器把"装得上吗、画得对吗"压成一个到处能跑的程序:换模型、转视角、切伽马、开阴影,
* 屏幕上按一下就有 —— 它同时也是 `#643`/`#649`/`#642` 的一个**人工冒烟口**。
*/
#include <vml_3d.h>
#include <vml_ui.h>
#include <math.h>
#include <stdio.h>
/* ══ 候选模型 ══════════════════════════════════════════════════════════════
* 依次试;**第一个装得上的**就是开场那个,`B` 在"装得上的"之间循环。
*
* ⚠⚠ **相对路径是按「程序的所在目录」解析的**(2026-10-08 实测 —— **不是按 cwd**):
* 本程序在 `<工作区>/examples/c/3d/`,所以**能命中的那条是 `models/xxx.glb`**(就近的那份);写成
* `examples/c/3d/models/xxx.glb` 会被拼成 `…/c/3d/examples/c/3d/models/xxx.glb`
* (宿主日志原话,以当时的夹具为例:`gltf_load:找不到 …(解析成 …/c/3d/examples/c/3d/models/Fox.glb)`)。
* 手机与桌面是**同一套解析**(同一份共享层代码)⇒ 头一条就够,后面几条是兜底。
* ⚠ 2026-10-09:`Fox.glb` **已从仓库删除**(它文件内自带 `asset.copyright` = **CC-BY 4.0**,要署名,
* 而仓里当时记成 CC0 ⇒ 无署名随包分发)。随仓库进的换成 **`rover_cutout.glb`**(我们自生成,无第三方权利)。
* Avocado / BoomBox / ToyCar 在本机磁盘上(`models/.gitignore` 拦在仓库外、但**会进 vml_lib.zip**),
* DamagedHelmet 从未入库。许可与署名逐份见 `models/CREDITS.md`。
* ⚠ 2026-10-10:**大模型全部删掉、换成小的**(用户:「例子不需要大文件,打包太大」)——
* `models/` 从 ~69 MB 降到 ~450 KB。删的是 >5 MB 的那几个 + 两个许可有问题的
* (`DamagedHelmet` CC BY-**NC** 禁商用 / `BoxTextured` 要署名带商标限制);
* 换进来的是 Khronos 的 **CC0** 小样例(`AnimatedColorsCube` / `AnimatedMorphCube` /
* `BoxVertexColors` / `InterpolationTest` / `SimpleInstancing` / `TextureLinearInterpolationTest`)。
* ⚠ 2026-10-10 又换了一轮:`Duck`(Khronos README 写的是 **SCEA Shared Source License**,
* 而本仓一直记成 CC0)与 `TextureCoordinateTest`(文件内 `asset.copyright` 写 **CC-BY 4.0**、
* README 却写 CC0 —— **两处冲突**)都**出局**了,见 `models/CREDITS.md`。
* 逐份许可见 `models/CREDITS.md`。
* ⚠ 2026-10-09: `Fox.glb` was **removed from the repo** ... see `CREDITS.md`.
* ⚠ 2026-10-10: the big models were **dropped for small ones** (user: "examples don't need big files,
* the package is too big") - `models/` went from ~69 MB to ~450 KB. */
static const char* CANDIDATES[] = {
/* ⚠⚠ **同一个相对路径在手机与桌面上指的不是同一个地方**(2026-10-08 实测,宿主日志原话):
* 手机:`models/xxx.glb` → `/…/workspace/models/xxx.glb` ← 基准 = **工作区根**
* 桌面:`models/xxx.glb` → `/…/Examples/c/3d/models/xxx.glb` ← 基准 = **程序目录**
* (手机那侧的规矩来自 Android 那条铁律:「一切路径以工作区为根、只按相对路径工作」。)
* ⇒ 每个模型都写**两种拼法**,哪端命中哪端算 —— 只写一种的话,另一端会静默找不到、
* 然后**悄悄退回下一个候选**(症状正是"我明明放了模型,它却打开的是别的")。
*
* 顺序 = 打开顺序(`B` 依次换)。**复杂的放前面**(用户口径:「再打开复杂一点的 glb」)。 */
/* ⚠ 2026-10-10:换成**随包的小模型**(全部 CC0)——见文件头那条,理由与逐份许可在
* `models/CREDITS.md`。原先那些(DamagedHelmet/BoomBox/Avocado)已删(太大 / 许可)。
* ⚠ `MetalRoughSpheresNoTextures` 试过但**装不上**(102 个网格 > 引擎上限 64,实测
* `gltf_load:解析失败 —— 网格数 102 超过引擎上限 64`)⇒ 没进包。 */
"models/AnimatedColorsCube.glb", /* 手机:<工作区>/models/… */
"examples/c/3d/models/AnimatedColorsCube.glb", /* 桌面:<程序目录>/models/… */
"models/TextureLinearInterpolationTest.glb",
"examples/c/3d/models/TextureLinearInterpolationTest.glb",
"models/InterpolationTest.glb",
"examples/c/3d/models/InterpolationTest.glb",
"models/AnimatedMorphCube.glb",
"examples/c/3d/models/AnimatedMorphCube.glb",
"models/SimpleInstancing.glb",
"examples/c/3d/models/SimpleInstancing.glb",
"models/BoxVertexColors.glb",
"examples/c/3d/models/BoxVertexColors.glb",
/* ⚠ 随仓库进的那一份 = `rover_cutout.glb`(**本仓库自己生成**,无第三方权利)。
* 2026-10-09 换的:原先这里兜底用的 `Fox.glb` 自带 `asset.copyright` = CC-BY 4.0(要署名),
* 而仓里把它记成了 CC0 ⇒ 已删。逐份许可见 `models/CREDITS.md`。
* ⚠ the one that **ships with the repo** is `rover_cutout.glb` (generated by us; no third-party rights). */
"rover_cutout.glb",
"models/rover_cutout.glb",
"examples/c/3d/rover_cutout.glb",
"third_party/vml/Examples/c/3d/rover_cutout.glb",
};
#define NCAND ((int)(sizeof(CANDIDATES) / sizeof(CANDIDATES[0])))
#define MAXOK 16
/** 装得上的那些:候选表下标 + 显示名(只取路径最后一段,HUD 上放不下整条路径)。
* ⚠ 名字摊平成**一维**数组按 96 一段手算下标 —— **别用二维 `char[MAXOK][96]`**:
* 实测这个 C 前端写进去读回来是空串(`printf` 打出 `(null)`),而**不报任何错**。 */
#define NAMELEN 96
static int OKIDX[MAXOK];
static char OKNAME[MAXOK * NAMELEN];
static int NOK;
/** 输出变换四档(`#649`):同一个场景在线性 / sRGB / Reinhard / ACES 下各长什么样。 */
static const char* GAMMA_NAME[] = { "线性 Linear", "sRGB", "Reinhard+sRGB", "ACES+sRGB" };
/** 底色五档。⚠ **白底那一档最容易看出模型自己的颜色对不对** —— 它是 sRGB 那套的判据载体。 */
static const int BGS[] = { 0xFF101018, 0xFF303038, 0xFF9A9AA2, 0xFFD8D8E0, 0xFF000000 };
#define NBG ((int)(sizeof(BGS) / sizeof(BGS[0])))
/** `0xAARRGGBB` 的构造(`g3_all.c` 里那个同名助手的精简版)—— 只在这里用得上。 */
static int argb(int r, int g, int b) { return -16777216 + r * 65536 + g * 256 + b; }
/** 取路径最后一段(`a/b/c.glb` → `c.glb`)。 */
static void tail_name(const char* p, char* out, int cap) {
int n; int i; int b;
n = 0; while (p[n] != 0) n = n + 1;
b = 0;
for (i = 0; i < n; i = i + 1) if (p[i] == '/' || p[i] == '\\') b = i + 1;
i = 0;
while (p[b + i] != 0 && i < cap - 1) { out[i] = p[b + i]; i = i + 1; }
out[i] = 0;
}
int main(void) {
int sw; int sh; int vp; int i;
int scene; int cur; int none; int bg; int gamma; int shadowOn; int autoOn; int pbrOn; int root;
float envK;
float camYaw; float camPitch; float camZoom; float modelR;
int touch[4]; int t0[4]; int t1[4];
int dragging; int pinching; int lastTx; int lastTy; float lastPinch;
char hud[200];
/* ⚠ 问尺寸必须用 **_ex**:`ui_scr_get_w()` 按**默认** chrome 估算,与本程序实际拿到的画布
* 对不上 ⇒ 视口贴左上角(`solar3d.c` 实测踩过)。 */
ui_win_open_ex("glb 查看器", 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 = 360;
if (sh <= 0) sh = 640;
vp = ui_3d_viewport_create(0, 0, sw, sh, 0);
if (vp <= 0) {
/* ⚠ **拿不到设备要如实报**:既不画、也不说,是最坏的一类失败。 */
printf("NO_3D_DEVICE=1\n");
return 0;
}
ui_3d_viewport_set_bg(vp, BGS[0]);
/* 主光 + 一侧补光(`#637`):只有一盏时模型暗面是纯黑、看不出形状。 */
ui_3d_viewport_set_light(vp, 0.45f, 0.85f, 0.35f, 0xFFFFFFFF, 0.30f);
ui_3d_viewport_set_light_at(vp, 1, -0.6f, 0.30f, -0.5f, 0x40FFFFFF, 0.0f);
/* ⚠⚠ **PBR 默认开着**(`#652`)。理由:glTF 的材质**天生是 PBR 的** —— 金属度/粗糙度那张图
* (`#661`)**只在 PBR 那条路上起作用**,关着 PBR 时它一个字节都进不了着色器 ⇒
* 一个金属头盔会被画成**漫反射色块**(用户 2026-10-08 实测就是这么报的:
* 「材质效果差很多了 / 玻璃贴图没出来」)。固定管线那条路是给**老程序**保底用的,
* 不是给 glTF 用的。 */
ui_3d_viewport_set_pbr(vp, 1);
/* ⚠⚠ **环境强度也要一起设**(`#662`)。PBR 里**金属的亮度几乎全部来自环境反射**,
* 而环境贴图是 8 位图(每通道封顶 1.0)、金属的 F0 又是暗色基色 ⇒ 只设 PBR 的话金属是
* "暗的镜子",看着发闷(用户 2026-10-08 口径:「还缺一些效果」「玻璃效果没有实现」)。
* 实测把天空画亮之后黑软管/金属边条立刻有了正确反射 ⇒ **根因就是这个倍率**。
* ⚠ 真实渲染器靠 **HDR 环境贴图**(太阳几百、天空几);本仓没有,所以用倍率代替
* (与 three.js 的 `scene.environmentIntensity` 同一个旋钮)。 */
envK = 1.8f;
ui_3d_viewport_set_env_intensity(vp, envK);
/* ── 探一遍候选 ────────────────────────────────────────────────────────
* ⚠ `ui_3d_gltf_load` 是"**边解析边建物体**",没有"只解析不建"的形态 ⇒ 试一条就得**立刻释放**
* (不释放的话每试一条,屏幕上就多叠一份模型)。
* ⚠ 这一趟的代价 = 每个**真的存在**的候选解析一次;不存在的(大多数)在打开文件那一步就
* 失败了,几乎不要钱。 */
/* ⚠ **第一个装上的直接留着用**,后面探到的立刻还回去 —— 这样每个模型只解析一次
* (探路 + 正式装 = 两趟的写法会让启动慢一倍,实测 Fox 那 163 KB 要几秒)。 */
NOK = 0; scene = 0; cur = -1;
for (i = 0; i < NCAND && NOK < MAXOK; i = i + 1) {
int s;
s = ui_3d_gltf_load(vp, (char*)CANDIDATES[i]);
if (s <= 0) continue;
if (NOK == 0) scene = s; else ui_3d_gltf_free(s);
OKIDX[NOK] = i;
tail_name(CANDIDATES[i], &OKNAME[NOK * NAMELEN], NAMELEN);
NOK = NOK + 1;
}
if (NOK > 0) cur = 0;
none = (NOK == 0 || scene <= 0);
if (none) { scene = 0; cur = -1; }
printf("GLBVIEW models=%d first=%s\n", NOK,
none ? "(none)" : &OKNAME[(cur < 0 ? 0 : cur) * NAMELEN]);
if (none) {
/* 一个都没装上:**说清楚试过哪些**,别留一块空屏让人猜。 */
printf("NO_MODEL=1\n");
for (i = 0; i < NCAND; i = i + 1) printf(" tried: %s\n", CANDIDATES[i]);
}
/* ── 程序化天空(**金属要靠它才不发黑**)─────────────────────────────────
* ⚠⚠ 这是本仓的一条硬事实:PBR 的金属**没有漫反射**,全部能量走镜面 —— 而镜面要有东西可反射。
* `DamagedHelmet` 的面罩是 `metalness = 1`,视口**没设天空盒**时环境反射那段一条指令都不走
* ⇒ 读对了金属度贴图**反而更暗**(实测:一整块黑疙瘩)。真实查看器靠 IBL 照环境,这里靠
* `#640` 那张天空盒(本仓的环境反射是"按反射方向采天空盒 + 按粗糙度挑 mip 级"的近似)。
* ⇒ 画一块**竖直渐变** → 用 `ui_2d_get_image` 抓成图像 → 当天空盒(等距圆柱)。
* ⚠ 抓的是**当时 2D 画布上的内容**,所以必须在画 HUD **之前**抓、抓完清屏。 */
{
int band; int skyH; int sky; int horizon;
skyH = sh / 2;
horizon = skyH * 45 / 100; /* 地平线放在 45% 高度处 */
ui_2d_clear(0);
/* ⚠⚠ **环境要有结构**,不能只是一条渐变:金属**没有漫反射**,它的样子**完全**来自
* 反射到的东西 —— 一片纯渐变天空反到抛光面罩上,看起来就是**一块发闷的平色**
* (用户 2026-10-08 实测:「还缺一些效果」)。给天空加**地平线 + 地面 + 一个太阳**
* 之后,同一块面罩上立刻能看出"天在上、地在下的弧"和那个亮点 ⇒ **一眼就是镜子**。
* (真实查看器靠 HDRI 环境贴图,这里用几笔画一个"认得出的世界"。) */
for (band = 0; band < 96; band = band + 1) {
int y; int t; int r0; int g0; int b0;
y = band * skyH / 96;
if (y < horizon) {
/* 上半:天顶深蓝 → 地平线灰白(地平线附近最亮) */
t = (horizon > 0) ? (y * 255 / horizon) : 0;
r0 = 40 + (205 - 40) * t / 255;
g0 = 62 + (215 - 62) * t / 255;
b0 = 110 + (228 - 110) * t / 255;
} else {
/* 下半:**地面**(暖灰,比天空暗一截)—— 没有它金属反射就只有"上面亮下面也亮" */
/* ⚠⚠ **地面要压得比天空暗很多** —— "玻璃/金属"的观感来自**对比**(亮的天 + 暗的地
* 在抛光面上切开一条清晰的地平线),而不是来自整体亮度。地面和天空一样亮的话,
* 反射出来还是一块平色(前面几版就是这么"提不亮也压不暗"的)。 */
t = (skyH - 1 - horizon > 0) ? ((y - horizon) * 255 / (skyH - 1 - horizon)) : 0;
r0 = 46 - (46 - 12) * t / 255;
g0 = 47 - (47 - 13) * t / 255;
b0 = 50 - (50 - 16) * t / 255;
}
ui_2d_rect(0, y, sw, skyH / 96 + 1, argb(r0, g0, b0), 1, 0, 0);
}
/* ── 让环境**有高频结构**(这一条决定"像不像玻璃")──────────────────────
* ⚠⚠ 只画一条**竖直渐变**的话,光滑球面上反射出来**还是一条渐变** —— 看着就是
* "一块平色 + 没有光泽"(用户 2026-10-08 反复说的「玻璃效果没出来」)。
* 真查看器靠 **HDRI 环境贴图**(天空、云、建筑、地面,全是有形状的东西),
* 弯曲的镜面把那堆形状**扭出可辨认的轮廓**,大脑立刻读成"玻璃"。
* ⇒ 这里用几笔画一个有形状的世界:**太阳 + 云 + 地平线亮带 + 暗地面**。
* ⚠ 顺序要紧:先地面/天空(底),再云,最后太阳(压在最上层)。 */
{
int k;
/* 地平线亮带:抛光面上那条最亮的水线就是它 */
ui_2d_rect(0, horizon - skyH / 40, sw, skyH / 20, argb(255, 248, 232), 1, 0, 0);
/* 几朵"云":圆角矩形 = 有形状的亮块(半径给到一半高就够圆了) */
for (k = 0; k < 5; k = k + 1) {
int cx; int cy; int cw; int ch;
/* ⚠⚠ **云要贴近地平线**:面罩(抛光面)反射到的主要是**地平线上下那一带**,
* 云画在天顶附近的话,镜面里**什么都反射不到** ⇒ 还是一块平色。 */
cx = (sw * (12 + k * 19)) / 100;
cy = horizon * (56 + ((k % 3) * 13)) / 100;
cw = sw / (7 + (k % 3));
ch = skyH / (16 + (k % 4));
ui_2d_rect(cx, cy, cw, ch, argb(232, 236, 244), 1, 0, ch / 2);
ui_2d_rect(cx + cw / 4, cy + ch / 2, cw * 3 / 5, ch * 2 / 3,
argb(214, 220, 232), 1, 0, ch / 3);
}
/* 地面上的几块暗斑:让"下半"也有形状(否则金属的下半球还是一块平色) */
for (k = 0; k < 4; k = k + 1) {
int gx; int gy; int gw; int gh;
gx = (sw * (8 + k * 26)) / 100;
gy = horizon + (skyH - horizon) * (20 + ((k % 2) * 40)) / 100;
gw = sw / 5; gh = skyH / 18;
ui_2d_rect(gx, gy, gw, gh, argb(38, 40, 44), 1, 0, gh / 2);
}
/* 太阳(最亮的那一点)—— 抛光面上那记高光就是它;偏左上,转起来才有变化 */
/* ⚠ **太阳要压到地平线附近**(夕阳位):低太阳在抛光面上拉出一条**又长又亮**的反射,
* 那是"金属/玻璃"最强的视觉信号;画在高空只会反射不到。 */
ui_2d_rect(sw * 24 / 100, horizon - skyH / 12, sw / 7, skyH / 8, argb(255, 252, 238), 1, 0, 0);
ui_2d_rect(sw * 20 / 100, horizon - skyH / 9, sw / 5, skyH / 6, argb(196, 204, 216), 1, 0, sw / 14);
}
sky = ui_2d_get_image(0, 0, sw, skyH);
ui_2d_clear(0);
if (sky > 0) ui_3d_viewport_set_skybox(vp, sky);
}
/* ── 自动取景:**先把模型归一化到固定大小**,再按这个固定半径取景 ──────────
* ⚠ 模型的大小是**数据**(Avocado 是 ±0.01、Fox 是 87.7、头盔是 1.6),而相机距离是**屏幕的函数**
* ⇒ 半径必须问 `#648` 要,不能在程序里硬写。
* ⚠⚠ **归一化不只是为了好看**:天空盒的半径是宿主常量(180 世界单位),而相机距离由 `fit`
* 按半径推 —— 一个半径 87.7 的模型会把相机推到 **~175**,离 180 只剩一点点余量;
* 再大一点模型就跑到天空盒**外面**、被盒子的后壁挡住("模型突然不见了")。
* 统一缩到 10 之后相机约 23,任何模型都稳稳地在盒子里。 */
modelR = 10.0f;
root = 0;
if (scene > 0) {
float bnd[4]; float r; float s;
bnd[0] = 0.0f; bnd[1] = 0.0f; bnd[2] = 0.0f; bnd[3] = 0.0f;
r = 1.0f;
if (ui_3d_gltf_bounds(scene, (int)&bnd[0]) == 0 && bnd[3] > 0.0f) r = bnd[3];
s = 10.0f / r;
root = ui_3d_gltf_root(scene);
if (root > 0) ui_3d_obj_set_scale(root, s);
modelR = r; /* HUD 上显示**原始**半径(那是数据的样子) */
}
ui_3d_viewport_fit(vp, 11.5f);
camYaw = 30.0f; camPitch = 18.0f; camZoom = 1.0f;
autoOn = 1; gamma = 0; bg = 0; shadowOn = 0; pbrOn = 1;
dragging = 0; pinching = 0; lastTx = 0; lastTy = 0; lastPinch = 0.0f;
while (ui_win_is_closed() == 0) {
int k;
/* ⚠ 自动旋转**累加到 yaw 上**(不是给物体设 `#623` 自转)—— 转的是**相机**,
* 于是模型背后那一面也能看到,而且不需要知道模型的朝向。 */
if (autoOn) camYaw = camYaw + 0.35f;
if (camYaw > 360.0f) camYaw = camYaw - 360.0f;
/* ── 单指拖 = 转视角 ───────────────────────────────────────────────
* 用「本帧 − 上一帧」的**增量**累加,不用绝对位移:手指从边缘重新落下时绝对量会跳。 */
if (ui_touch_read(0, touch)) {
if (touch[2]) {
if (dragging) {
/* ⚠ **横向取负号**:`set_orbit` 的 yaw 是球坐标标准式(与 three.js 的
* `OrbitControls` 同一条),而那边拖右是"把画面往右带" ⇒ θ 减小。
* 竖直不用翻:pitch 增大让相机升高、画面跟着手指往下走。 */
camYaw = camYaw - (touch[0] - lastTx) * 0.5f;
camPitch = camPitch + (touch[1] - lastTy) * 0.5f;
if (camPitch > 88.0f) camPitch = 88.0f;
if (camPitch < -88.0f) camPitch = -88.0f;
}
dragging = 1; lastTx = touch[0]; lastTy = touch[1];
} else dragging = 0;
} else dragging = 0;
/* ── 两指开合 = 缩放 ───────────────────────────────────────────────
* 用**两指间距的比值**累乘(同"增量"那条理由)。⚠ 比值要挡小值:两指几乎重合时
* `lastPinch` 接近 0 ⇒ 除零得 inf ⇒ 画面直接没了。 */
if (ui_touch_read(0, t0) && ui_touch_read(1, t1) && t0[2] && t1[2]) {
float ddx; float ddy; float dd;
ddx = (float)(t0[0] - t1[0]);
ddy = (float)(t0[1] - t1[1]);
dd = (float)sqrt((double)(ddx * ddx + ddy * ddy));
if (pinching && dd > 8.0f && lastPinch > 8.0f) {
camZoom = camZoom * (lastPinch / dd);
if (camZoom < 0.15f) camZoom = 0.15f;
if (camZoom > 6.0f) camZoom = 6.0f;
}
lastPinch = dd; pinching = 1;
} else pinching = 0;
ui_3d_viewport_set_zoom(vp, camZoom);
ui_3d_viewport_set_orbit(vp, camYaw, camPitch);
/* ── 键盘(手机上来自自带手柄)───────────────────────────────────── */
if (ui_key_is_down(VML_KEY_PAD_A)) autoOn = 1 - autoOn;
if (ui_key_is_down(VML_KEY_SELECT)) {
pbrOn = 1 - pbrOn;
ui_3d_viewport_set_pbr(vp, pbrOn);
}
if (ui_key_is_down(VML_KEY_PAD_Y)) {
shadowOn = 1 - shadowOn;
ui_3d_viewport_set_shadow(vp, shadowOn);
}
if (ui_key_is_down(VML_KEY_PAD_X)) {
gamma = gamma + 1; if (gamma > 3) gamma = 0;
ui_3d_viewport_set_gamma(vp, gamma);
}
/* ↑/↓ = 环境强度(`#662`)。⚠ 这两个键原来调**背景色**,而天空盒(`#640`)一开就把背景
* **整个盖住**了 ⇒ 它们成了**死键**(按下去画面纹丝不动)。
* 改成调环境强度 —— 这一屏上**最值得调**的就它(金属亮不亮全看这个数)。 */
if (ui_key_is_down(VML_KEY_UP)) {
envK = envK + 0.4f; if (envK > 8.0f) envK = 8.0f;
ui_3d_viewport_set_env_intensity(vp, envK);
}
if (ui_key_is_down(VML_KEY_DOWN)) {
envK = envK - 0.4f; if (envK < 0.0f) envK = 0.0f;
ui_3d_viewport_set_env_intensity(vp, envK);
}
if (ui_key_is_down(82)) { /* 'R' —— 复位视角 */
camYaw = 30.0f; camPitch = 18.0f; camZoom = 1.0f;
}
/* B = 换模型:释放当前、装下一个能装上的 */
if (ui_key_is_down(VML_KEY_PAD_B) && NOK > 1) {
int nxt; int s; int nroot;
nxt = cur + 1; if (nxt >= NOK) nxt = 0;
s = ui_3d_gltf_load(vp, (char*)CANDIDATES[OKIDX[nxt]]);
if (s > 0) {
if (scene > 0) ui_3d_gltf_free(scene); /* ⚠ 先装新的、装上了再放旧的:
* 反过来的话新模型装失败就**全空**了 */
scene = s; cur = nxt;
{
float bnd[4]; float r; float s;
bnd[3] = 0.0f;
r = 1.0f;
if (ui_3d_gltf_bounds(scene, (int)&bnd[0]) == 0 && bnd[3] > 0.0f) r = bnd[3];
s = 10.0f / r;
nroot = ui_3d_gltf_root(scene);
if (nroot > 0) ui_3d_obj_set_scale(nroot, s);
modelR = r;
}
ui_3d_viewport_fit(vp, 11.5f);
camZoom = 1.0f;
}
}
/* ── HUD:2D 画在 3D **之上** ─────────────────────────────────────
* `ui_2d_clear` 用全透明 —— 出图时 3D 那一层是底、2D 图元直接叠上去。 */
ui_2d_clear(0);
ui_2d_text(14, 12, "glb 查看器", -1, 17, VML_ANCHOR_LEFT);
if (none) {
ui_2d_text(14, 40, "没有找到模型 —— 把 .glb 放到 examples/models/ 再跑", -65536, 12,
VML_ANCHOR_LEFT);
} else {
int n;
n = ui_3d_gltf_object_count(scene);
sprintf(hud, "%s 物体 %d 半径 %.1f", &OKNAME[cur * NAMELEN], n, (double)modelR);
ui_2d_text(14, 40, hud, -8355712, 12, VML_ANCHOR_LEFT);
}
sprintf(hud, "%s%s PBR%s 阴影%s 环境 ×%.1f",
GAMMA_NAME[gamma], autoOn ? " 自动旋转" : "", pbrOn ? "开" : "关",
shadowOn ? "开" : "关", (double)envK);
ui_2d_text(14, 60, hud, -8355712, 12, VML_ANCHOR_LEFT);
ui_2d_text(14, 80, "拖动旋转 · 两指缩放 · A 自转 · B 换模型 · X 伽马 · Y 阴影 · SELECT PBR · ↑↓ 环境 · R 复位",
-8355712, 11, VML_ANCHOR_LEFT);
ui_2d_present();
/* ⚠ 这一句只是"等输入、最多等这么久";**上屏节奏由宿主管**
* (`ui_2d_present` 出来时宿主已经跟着显示走了一帧)—— 别靠改这个数去调帧率。 */
k = ui_msg_fetch_wait(40);
(void)k;
}
if (scene > 0) ui_3d_gltf_free(scene);
ui_3d_viewport_free(vp);
return 0;
}