c3d-web-renderer
v1.1.3
Published
3D model renderer with C++/WASM — supports OBJ and STEP/STP formats (bundled OCCT)
Maintainers
Readme
C3D Web Renderer
基于 C++ / WASM / WebGL 的 3D 模型渲染器,支持 OBJ 与 STEP/STP。
功能特性
- 金属哑光灰材质 — 偏白冷灰金属质感 + 哑光表面处理
- 选择性描边 — 边界边与特征边(锐角 > 30°)
- AABB 包围体 — 2% 内边距
- 尺寸标注 — 长/宽/高(原始值 × 100000)
- 模型透明 / 重置视图 / 坐标轴显示
- OBJ 分块读入 — 文本按 64KB、行边界切分,逐块
appendOBJChunk,块间让出主线程 - OBJ 分步后处理 — 法线/描边/AABB 通过
stepFinalizeOBJ分批完成,并回调onProgress
安装
npm install c3d-web-rendererVite 配置
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
function wasmMimePlugin() {
return {
name: 'wasm-mime',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url?.split('?')[0].endsWith('.wasm')) {
res.setHeader('Content-Type', 'application/wasm');
}
next();
});
}
};
}
export default defineConfig({
plugins: [react(), wasmMimePlugin()],
assetsInclude: ['**/*.wasm'],
optimizeDeps: {
exclude: ['c3d-web-renderer']
}
});快速使用
1. 准备 Canvas
HTML 中放一个全屏或固定尺寸的 <canvas>,父容器建议 position: relative(尺寸标注会叠加在 canvas 上):
<div style="position: relative; width: 100vw; height: 100vh;">
<canvas id="viewer" style="width: 100%; height: 100%; display: block;"></canvas>
</div>2. Vite 项目中导入 WASM 路径
WASM 不能被打进 JS bundle,需用 ?url 拿到实际 URL,并同时传入 .wasm 与 .js:
import wasmUrl from 'c3d-web-renderer/wasm/c3d_core.wasm?url';
import wasmJsUrl from 'c3d-web-renderer/wasm/c3d_core.js?url';3. 原生 JavaScript 完整示例(OBJ)
import { createRenderer, CAMERA_PERSPECTIVE } from 'c3d-web-renderer';
import wasmUrl from 'c3d-web-renderer/wasm/c3d_core.wasm?url';
import wasmJsUrl from 'c3d-web-renderer/wasm/c3d_core.js?url';
const canvas = document.getElementById('viewer');
// 创建渲染器(内部加载 WASM 并初始化 WebGL)
const renderer = await createRenderer(canvas, {
wasmUrl,
wasmJsUrl,
modelColor: [0.99, 0.99, 0.99],
backgroundColor: [1.0, 1.0, 1.0],
camera: CAMERA_PERSPECTIVE,
alpha: 1.0,
showOutline: true,
showAABB: false,
showAxes: false,
showLabels: false
});
// 启动渲染循环(拖拽旋转、滚轮缩放)
renderer.start();
// 从 URL 加载 OBJ:下载 → 分块解析 → 分步后处理 → 上传 GPU
await renderer.loadOBJFromUrl('https://example.com/models/part.obj', {
onProgress: (e) => {
if (e.phase === 'download') {
const pct = e.total ? Math.round((e.loaded / e.total) * 100) : null;
console.log('下载', pct != null ? `${pct}%` : '...');
return;
}
if (e.done != null && e.total) {
console.log(e.phase, `${Math.round((e.done / e.total) * 100)}%`);
return;
}
console.log(e.phase, '...');
}
});
// 交互:点击拾取面/边
renderer.setOnPick((hit) => {
if (!hit) return;
if (hit.type === 'face') {
// hit.faceId 为已选面号数组(无上限),hit.clickedFaceId 为本次点击的面
renderer.setSurfaceColor([hit.clickedFaceId], [1, 0.4, 0.4]);
}
});
// 视图控制
renderer.fitToView(); // 适应画布
renderer.setAlpha(0.8); // 半透明
renderer.setShowAABB(true);
// 页面卸载时释放资源
window.addEventListener('beforeunload', () => renderer.destroy());4. 加载 STEP/STP
STEP 走 OCCT 细分 + C++ 后处理,用法与 OBJ 相同,仅换加载方法:
await renderer.loadSTEPFromUrl('https://example.com/models/part.stp', {
onProgress: (e) => console.log(e.phase, e.done ?? e.loaded, e.total)
});5. 已有文本内容时(非 URL)
// OBJ:传入完整 .obj 文本字符串
const objText = await fetch('/models/part.obj').then((r) => r.text());
await renderer.loadOBJ(objText, {
onProgress: (e) => console.log(e.phase, e.done, e.total)
});
// STEP:传入完整 .stp 文本字符串
const stepText = await fetch('/models/part.stp').then((r) => r.text());
await renderer.loadSTEP(stepText, {
onProgress: (e) => console.log(e.phase, e.done, e.total)
});6. 取消加载
传入 AbortSignal,可在用户切换模型时中断下载或解析:
const controller = new AbortController();
loadPromise = renderer.loadOBJFromUrl(url, {
signal: controller.signal,
onProgress: (e) => { /* ... */ }
});
// 需要取消时:
controller.abort();7. React 示例
import { useEffect, useRef, useState } from 'react';
import { createRenderer, CAMERA_PERSPECTIVE } from 'c3d-web-renderer';
import wasmUrl from 'c3d-web-renderer/wasm/c3d_core.wasm?url';
import wasmJsUrl from 'c3d-web-renderer/wasm/c3d_core.js?url';
export default function Viewer() {
const canvasRef = useRef(null);
const [status, setStatus] = useState('');
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
let cancelled = false;
let renderer = null;
(async () => {
const r = await createRenderer(canvas, {
wasmUrl,
wasmJsUrl,
camera: CAMERA_PERSPECTIVE,
showOutline: true
});
if (cancelled) {
r.destroy();
return;
}
renderer = r;
r.start();
await r.loadOBJFromUrl('https://example.com/part.obj', {
onProgress: (e) => {
if (!cancelled) setStatus(e.phase ?? 'loading');
}
});
})().catch(console.error);
return () => {
cancelled = true;
renderer?.destroy();
};
}, []);
return (
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
<canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />
{status && <div style={{ position: 'absolute', top: 12, left: 12 }}>{status}</div>}
</div>
);
}8. 常用操作速查
| 操作 | 代码 |
|------|------|
| 启动/停止渲染循环 | renderer.start() / renderer.stop() |
| 适应视图 | renderer.fitToView() |
| 重置相机 | renderer.resetView() |
| 切换透视/正交 | renderer.setCamera(CAMERA_PERSPECTIVE) |
| 读取尺寸 | renderer.getDimensions() |
| 销毁实例 | renderer.destroy() |
完整 Demo 见仓库 Demo/react(OBJ)与 Demo/step-react(STEP)。
解析流程
OBJ(有分块读入)
fetch 下载完整文本
→ splitObjTextIntoChunks(64KB,按行切分)
→ beginParseOBJ + appendOBJChunk(逐块,phase: parse)
→ stepFinalizeOBJ(分步后处理,phase: faceNormals / vertexNormals / …)
→ 渲染- 有分块读入:
loadOBJ/loadOBJFromUrl内部自动分块,每块之间yield主线程 - 不是边下边解析:先下载完整个文件,再分块送入 WASM
- 也可手动调用
loadOBJChunks(chunks)或底层beginParseOBJ+appendOBJChunk
STEP
fetch 下载
→ tessellateSTEP(OCCT 细分)
→ loadFromRawMesh(C++ 后处理)
→ 渲染API
createRenderer(canvas, options?)
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| wasmUrl | string | — | c3d_core.wasm URL(Vite 用 ?url 导入) |
| wasmJsUrl | string | — | c3d_core.js URL,用于正确定位 WASM 资源 |
| alpha | number | 1.0 | 透明度 |
| showOutline | boolean | true | 描边 |
| showAABB | boolean | true | 包围体 |
| showAxes | boolean | true | 坐标轴 |
| showLabels | boolean | true | 尺寸标注 |
| modelColor | [r,g,b] | [0.99,0.99,0.99] | 模型颜色 |
| backgroundColor | [r,g,b] | [1,1,1] | 背景颜色 |
| camera | CameraType | perspective | 相机类型 |
| autoHighlightOnPick | boolean | true | 点击高亮 |
ObjRenderer
| 方法 | 说明 |
|------|------|
| loadOBJ(text, { onProgress?, signal? }) | 分块读入 + 分步后处理 |
| loadOBJFromUrl(url, { onProgress?, signal? }) | 下载后同上 |
| loadOBJChunks(chunks) | 直接传入已切好的文本块数组 |
| loadSTEP(text, { onProgress?, signal? }) | OCCT 细分 + C++ 后处理 |
| loadSTEPFromUrl(url, …) | 下载后加载 STEP |
| setAlpha / setCamera / fitToView / resetView | 视图控制 |
| setSurfaceColor(faceIds, [r,g,b]) / setSurfaceColors / clearSurfaceColor | 按 CAD 面号整面设色(faceIds 为面号数组) |
| setOnPick / pickAt | 拾取面返回 hit.faceId(已选面号数组)与 hit.clickedFaceId(本次点击面) |
| start / stop / destroy | 生命周期 |
onProgress 阶段
| phase | 含义 |
|-------|------|
| download | 网络下载(loaded / total) |
| parse | OBJ 分块读入(done / total = 块序号) |
| tessellate | STEP 面细分 |
| faceNormals / vertexNormals / outline / aabb / done | C++ 后处理 |
低级 API
import {
loadWasm,
fetchModelText,
tessellateSTEP
} from 'c3d-web-renderer';C++ WASM 进阶导出:
beginParseOBJ()/appendOBJChunk(text, isLast)beginFinalizeOBJSteps()/stepFinalizeOBJWithProgress(budget, onProgress)— 返回0继续 /1完成 /-1失败loadFromRawMesh(positions, indices, faceIds)— STEP 网格后处理
发布文件
| 路径 | 说明 |
|------|------|
| dist/c3d-web-renderer.esm.js | ESM 入口 |
| dist/wasm/c3d_core.wasm | C++ 几何引擎 |
| dist/wasm/c3d_core.js | WASM glue |
| dist/wasm/replicad_single.wasm | STEP 细分 |
| dist/wasm/replicad_single.js | OCCT glue |
