当 Three.js 遇上百万级点云,Web 内存与计算双双告急。我们用 C++ 扛起计算,用两种架构打通数据共享——同进程 N-API 与跨进程 gRPC+mmap,究竟谁更胜一筹?
引言
点云是三维世界中最原始、最直观的数据形式。一个中等规模的激光雷达扫描,动辄数百万乃至上亿个点。在 Electron 中直接使用 Three.js 加载 PLY/PCD 文件,往往瞬间耗尽内存,帧率跌至个位数。
根本原因在于:JavaScript 的单线程与 GC 压力,以及大块几何数据在 JS 堆中的双重拷贝。为了破局,业界普遍将计算密集、内存敏感的任务下沉到 C++,然后通过高效的数据共享机制将处理好的顶点数据传递给 Three.js。
本文将深入剖析两种架构方案:
- 同进程 N-API:C++ 代码以 Node 原生模块的形式直接运行在 Electron 主进程或渲染进程中。
- 跨进程 gRPC + mmap:C++ 作为独立后台服务,通过 gRPC 通信,通过 mmap 共享内存零拷贝交换数据。
我们不仅会对比优劣,更会给出核心代码实现,让你能真正落地到自己的项目中。
一、痛点与需求
以一个大点云项目为例:
- 点云文件:
.las格式,包含 2000 万个点,每个点有 XYZ、RGB、强度等属性。 - 需求:实时旋转/缩放,无卡顿;支持动态筛选(按强度、分类值)。
- 瓶颈:
- JS 解析 2000 万个点 → 内存爆炸(每点至少 24 字节,仅位置就需要 480MB)
- Three.js
BufferGeometry创建过程会再拷贝一次 → 内存翻倍 - 主线程解析 + 渲染 → UI 冻结
因此,必须:
- C++ 负责解析、滤波、LOD 生成。
- C++ 与 JS 共享同一块内存,避免数据拷贝。
- Three.js 直接消费共享内存中的顶点数据。
二、方案一:同进程 N-API(Node Addon)
2.1 架构图
┌─────────────────────────────────────────┐
│ Electron Main/Renderer │
│ ┌───────────────────────────────────┐ │
│ │ React UI │ │
│ └───────────────┬───────────────────┘ │
│ │ 调用 N-API 导出函数 │
│ ┌───────────────▼───────────────────┐ │
│ │ C++ Addon (N-API) │ │
│ │ - 点云解析 │ │
│ │ - 滤波/采样 │ │
│ │ - LOD 生成 │ │
│ └───────────────┬───────────────────┘ │
│ │ 返回 ArrayBuffer │
│ ┌───────────────▼───────────────────┐ │
│ │ Three.js Renderer │ │
│ │ BufferAttribute 直接引用 ArrayBuffer│
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
2.2 核心实现:C++ Addon 返回可转移的 ArrayBuffer
步骤 1:编写 C++ 点云解析函数
使用 N-API 创建 ArrayBuffer,填充顶点数据后返回给 JS。
#include <napi.h>
#include <vector>
#include <fstream>
#include "lasreader.hpp" // LASlib 等
struct Point {
float x, y, z;
uint8_t r, g, b;
};
Napi::Value ParsePointCloud(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string filepath = info[0].As<Napi::String>().Utf8Value();
// 1. C++ 中解析点云,获取点数量
LASreader* reader = LASreader::open(filepath.c_str());
uint64_t num_points = reader->npoints;
// 2. 计算内存大小(每个点 3*float + 3*uint8 = 12+3 = 15 字节,对齐到 16 字节)
size_t buffer_size = num_points * sizeof(Point);
// 3. 创建 N-API ArrayBuffer,内存由 C++ 分配(可使用 napi_create_external_arraybuffer 避免额外拷贝)
void* data = malloc(buffer_size);
Point* points = static_cast<Point*>(data);
// 4. 填充数据
size_t idx = 0;
while (reader->read_point()) {
points[idx].x = reader->point.get_x();
points[idx].y = reader->point.get_y();
points[idx].z = reader->point.get_z();
points[idx].r = reader->point.get_r();
points[idx].g = reader->point.get_g();
points[idx].b = reader->point.get_b();
++idx;
}
reader->close();
delete reader;
// 5. 创建 ArrayBuffer,并绑定释放回调
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, data, buffer_size,
[](Napi::Env env, void* finalize_data) {
free(finalize_data);
});
// 6. 返回给 JS(同时可附带点数量等元数据)
Napi::Object result = Napi::Object::New(env);
result.Set("buffer", buffer);
result.Set("numPoints", Napi::Number::New(env, num_points));
return result;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("parsePointCloud", Napi::Function::New(env, ParsePointCloud));
return exports;
}
NODE_API_MODULE(pointcloud_addon, Init)
步骤 2:React + Three.js 中消费 ArrayBuffer
// 在渲染进程中(确保 nodeIntegration 开启或通过 preload 暴露)
const addon = require('pointcloud-addon');
async function loadPointCloud(filePath) {
const { buffer, numPoints } = addon.parsePointCloud(filePath);
// 关键:将 ArrayBuffer 转为 Float32Array 和 Uint8Array 视图,但不复制数据
const positions = new Float32Array(buffer, 0, numPoints * 3);
const colors = new Uint8Array(buffer, numPoints * 12, numPoints * 3);
// 创建 Three.js BufferGeometry
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
// 使用 PointsMaterial 渲染
const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.1 });
const points = new THREE.Points(geometry, material);
scene.add(points);
}
2.3 优缺点分析
| 优点 | 缺点 |
|---|---|
| ✅ 零拷贝:C++ 分配的 ArrayBuffer 直接被 Three.js 使用,无内存复制 | ❌ 主线程阻塞:若直接在渲染进程调用会卡 UI(但可通过 worker_threads 解决) |
| ✅ 延迟极低:函数调用开销微秒级 | ❌ 崩溃风险:C++ Addon 内存越界会导致整个 Electron 进程崩溃 |
| ✅ 开发简单:无需跨进程通信,调试方便 | ❌ Node 版本绑定:需要针对 Electron 的 Node 版本编译原生模块 |
| ✅ 部署单一:只有一个 .exe/.app 文件 | ❌ 内存释放不可控:依赖 GC 触发 finalize,大对象可能延迟释放 |
关于 UI 阻塞的解决方案:
直接在渲染进程中调用 addon.parsePointCloud 会同步执行 C++ 代码,若解析耗时超过 16ms,页面就会掉帧。正确的做法是将解析任务放到主进程的 worker_threads 中执行,解析完成后通过 postMessage 将 ArrayBuffer 传回渲染进程(结构化克隆会转移所有权,依然零拷贝)。
javascript
复制下载
// 主进程中创建一个 worker 线程
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
const addon = require('pointcloud-addon');
parentPort.on('message', (filePath) => {
const { buffer, numPoints } = addon.parsePointCloud(filePath);
// 直接转移 ArrayBuffer 所有权,无需拷贝
parentPort.postMessage({ buffer, numPoints }, [buffer]);
});
`, { eval: true });
worker.on('message', ({ buffer, numPoints }) => {
// 通过 IPC 发送给渲染进程
mainWindow.webContents.send('pointcloud-data', buffer, numPoints);
});
渲染进程收到后,直接使用 new Float32Array(buffer) 创建视图即可。这样 C++ 解析完全在后台线程,UI 永不阻塞。
注意:
worker_threads是 Node.js 的线程,不是 Web Worker。Web Worker 无法加载原生模块,因此不适用于此场景。
C++ addon 调用放在 worker_threads 中执行 能避免 程序崩溃吗?
不能!
为什么 worker_threads 也无法隔离崩溃?
即便将 C++ addon 调用放在 worker_threads 中执行,由于 worker 线程与主进程共享同一内存空间,原生代码的崩溃依然会连带杀死整个进程。Worker 线程的隔离是 JS 层面的,而非操作系统级的进程隔离。
三、方案二:独立 C++ 服务 + gRPC + mmap 共享内存
3.1 架构图
┌─────────────────────────┐ gRPC(控制面) ┌─────────────────────────┐
│ Electron 主进程 │◄────────────────────────────►│ 独立 C++ 服务 │
│ ┌─────────────────────┐ │ │ - 点云解析 │
│ │ gRPC Client │ │ │ - 滤波/重采样 │
│ │ (Node.js) │ │ │ - LOD 生成 │
│ └──────────┬──────────┘ │ │ - 写入 mmap │
│ │ IPC │ └────────────┬────────────┘
│ ┌──────────▼──────────┐ │ │
│ │ Renderer (React) │ │ ┌────────────▼────────────┐
│ │ - Three.js │ │ mmap 共享内存(数据面) │ /dev/shm/pointcloud │
│ │ - 读取 mmap 数据 │◄─────────────────────────────►│ (顶点 + 索引 + 元数据) │
│ └─────────────────────┘ │ └─────────────────────────┘
└─────────────────────────┘
3.2 核心实现:三步骤打通数据流
3.2.1 C++ 服务:解析点云并写入 mmap
使用 boost::interprocess 或 POSIX shm_open + mmap。为了跨平台,推荐使用 boost。
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <atomic>
struct SharedPointCloudHeader {
std::atomic<uint64_t> version{0};
std::atomic<bool> ready{false};
uint64_t num_points;
uint64_t data_offset; // 顶点数据起始偏移
};
class PointCloudServer {
public:
void LoadAndShare(const std::string& las_path) {
// 1. 解析点云到内存 vector
std::vector<Point> points = ParseLAS(las_path);
// 2. 计算总大小
size_t header_size = sizeof(SharedPointCloudHeader);
size_t data_size = points.size() * sizeof(Point);
size_t total_size = header_size + data_size;
// 3. 创建共享内存对象
boost::interprocess::shared_memory_object shm(
boost::interprocess::open_or_create,
"pointcloud_shm",
boost::interprocess::read_write
);
shm.truncate(total_size);
// 4. 映射到本进程地址空间
boost::interprocess::mapped_region region(shm, boost::interprocess::read_write);
// 5. 写入 header
SharedPointCloudHeader* header = static_cast<SharedPointCloudHeader*>(region.get_address());
header->num_points = points.size();
header->data_offset = header_size;
header->version.fetch_add(1, std::memory_order_release);
// 6. 写入顶点数据
void* data_ptr = static_cast<char*>(region.get_address()) + header_size;
memcpy(data_ptr, points.data(), data_size);
// 7. 标记 ready
header->ready.store(true, std::memory_order_release);
}
// gRPC 服务接口:返回共享内存名称和大小
grpc::Status LoadModel(grpc::ServerContext*, const LoadRequest* req, LoadResponse* resp) {
LoadAndShare(req->file_path());
resp->set_shm_name("pointcloud_shm");
resp->set_shm_size(total_size);
resp->set_data_offset(header_size);
return grpc::Status::OK;
}
};
3.2.2 Electron 主进程:gRPC 调用获取元数据
// main.js 中
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { ipcMain } = require('electron');
const packageDefinition = protoLoader.loadSync('pointcloud.proto');
const proto = grpc.loadPackageDefinition(packageDefinition);
const client = new proto.PointCloudService('localhost:50051', grpc.credentials.createInsecure());
ipcMain.handle('load-pointcloud', async (event, filePath) => {
return new Promise((resolve, reject) => {
client.LoadModel({ file_path: filePath }, (err, response) => {
if (err) reject(err);
else resolve({
shmName: response.shm_name,
shmSize: response.shm_size,
dataOffset: response.data_offset
});
});
});
});
3.2.3 渲染进程:通过 mmap-io 读取共享内存并传给 Three.js
// 渲染进程中(通过 preload 暴露的 API)
const mmap = require('@cathodique/mmap-io');
const fs = require('fs');
async function loadAndRender(filePath) {
// 1. 通过 IPC 触发 C++ 服务加载
const { shmName, shmSize, dataOffset } = await window.electronAPI.loadPointcloud(filePath);
// 2. 打开共享内存(Linux /dev/shm,Windows 不同)
const fd = fs.openSync(`/dev/shm/${shmName}`, 'r');
const buffer = mmap.map(fd, mmap.PROT_READ, mmap.MAP_SHARED, shmSize, 0);
// 3. 解析 header(前 24 字节)
const version = buffer.readBigUInt64LE(0);
const ready = buffer.readUInt8(8) === 1;
const numPoints = Number(buffer.readBigUInt64LE(16));
if (!ready) throw new Error('Data not ready');
// 4. 从 dataOffset 位置读取顶点数据
const pointSize = 32; // 假设 Point 结构体大小
const positions = new Float32Array(numPoints * 3);
const colors = new Uint8Array(numPoints * 3);
for (let i = 0; i < numPoints; i++) {
const base = dataOffset + i * pointSize;
positions[i*3] = buffer.readFloatLE(base);
positions[i*3+1] = buffer.readFloatLE(base + 4);
positions[i*3+2] = buffer.readFloatLE(base + 8);
colors[i*3] = buffer.readUInt8(base + 12);
colors[i*3+1] = buffer.readUInt8(base + 13);
colors[i*3+2] = buffer.readUInt8(base + 14);
}
// 5. 创建 Three.js 几何体(注意:这里从 buffer 拷贝到了新 ArrayBuffer,可优化?见下文)
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
const points = new THREE.Points(geometry, new THREE.PointsMaterial({ vertexColors: true }));
scene.add(points);
}
性能陷阱:上面代码中,
positions和colors是从 mmap buffer 中逐点读取并创建的新 Float32Array,这仍然存在一次拷贝。要实现真正的零拷贝,需要让 Three.js 直接使用 mmap 映射的原始 buffer。但 Three.js 的BufferAttribute只接受ArrayBuffer或Buffer视图,并且要求该内存生命周期与几何体一致。我们可以利用SharedArrayBuffer或者直接传递 mmap 得到的Buffer对象,只要保证在几何体销毁前不被 unmap。
// 零拷贝版本:直接使用 mmap 返回的 Buffer 创建 Float32Array 视图
const totalFloats = numPoints * 3;
const positionsView = new Float32Array(buffer, dataOffset, totalFloats);
const colorsView = new Uint8Array(buffer, dataOffset + totalFloats * 4, numPoints * 3);
geometry.setAttribute('position', new THREE.BufferAttribute(positionsView, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colorsView, 3, true));
// 注意:需要确保 buffer 在几何体使用期间一直保持映射,不能提前 unmap
3.3 优缺点分析
| 优点 | 缺点 |
|---|---|
| ✅ 进程隔离:C++ 崩溃不影响 Electron UI | ❌ 架构复杂:需要管理 C++ 服务的生命周期 |
| ✅ 非阻塞渲染:解析在后台进行,UI 可显示进度 | ❌ 部署麻烦:需打包两个可执行文件 |
| ✅ 真正零拷贝:mmap 让多进程共享同一物理内存 | ❌ 跨平台兼容性:Windows 下 mmap 行为不同,需封装 |
| ✅ 可扩展:未来可升级为远程服务,支持多机集群 | ❌ 调试困难:gRPC + mmap 联合调试工具链不成熟 |
| ✅ 内存可控:C++ 服务可独立释放内存 | ❌ 延迟略高:首次加载需 gRPC 调用(~1ms) |
适用场景:点云规模极大(5000 万点以上),需要后台预处理、多任务排队,且对 UI 流畅度要求严苛。
四、核心问题:Buffer 如何从 C++ 共享到 Three.js?
无论哪种方案,最终目标都是让 Three.js 的 BufferAttribute 能直接访问 C++ 中分配的内存,避免复制。两种方案的技术本质:
- N-API 方案:C++ 通过
napi_create_external_arraybuffer分配内存,JS 拿到ArrayBuffer后,Three.js 可直接创建BufferAttribute。内存所有权归 JS(GC 时调用 free)。 - mmap 方案:C++ 与 JS 通过操作系统共享内存机制映射同一块物理内存,JS 侧通过
Buffer或SharedArrayBuffer访问。内存所有权归 OS,双方均可读写。
技术细节对比
| 方面 | N-API 外部 ArrayBuffer | mmap 共享内存 |
|---|---|---|
| 数据拷贝次数 | 0 次(C++ 直接写入 ArrayBuffer 内存) | 0 次(双方映射同一页) |
| 内存释放 | 由 JS GC 触发 finalize 回调 | 显式调用 munmap 或进程退出时释放 |
| 并发安全 | 单进程,无需额外同步 | 多进程,必须使用原子操作或互斥锁 |
| 跨语言友好 | 仅限 Node.js 环境 | 任何支持 POSIX API 的语言 |
| 最大数据量 | 受 V8 堆限制(64 位下约 4GB) | 受物理内存 + 操作系统限制 |
| 实现复杂度 | 低(N-API 标准接口) | 高(需处理权限、命名冲突、多进程同步) |
结论:对于绝大多数桌面端点云应用,N-API 方案已经足够,且更简单。只有当点云超过 4GB 或需要多进程同时访问时才考虑 mmap。
五、实战决策:我应该选哪种?
5.1 决策树
是否单点云超过 2GB?
├─ 是 → mmap 方案(突破 V8 堆限制)
└─ 否 → 是否需要支持多进程并发访问?
├─ 是 → mmap 方案
└─ 否 → 是否可接受 C++ 模块崩溃导致 Electron 闪退?
├─ 是 → N-API 方案(最简单)
└─ 否 → mmap 方案(进程隔离更安全)
5.2 混合方案:按需选择
实际项目中,可采用双模式:默认使用 N-API(性能最优),当检测到点云过大时,降级为独立服务模式。
async function loadPointCloud(filePath) {
const fileSize = getFileSize(filePath);
if (fileSize < 1e9) { // < 1GB
return loadWithNapi(filePath);
} else {
return loadWithGrpcMmap(filePath);
}
}
好的,我将把“如何实现内存释放”和“clear 如何触发调用”这两个回答整合成一篇完整的技术说明,保持逻辑连贯,避免重复。
六, 跨进程方案(gRPC + mmap)中的内存释放与触发机制
在 gRPC + mmap 架构中,共享内存的生命周期由 C++ 服务端和 Electron 客户端共同管理。内存释放不是单个操作,而是一套需要双方配合的流程。下面分两部分阐述:释放操作本身 和 释放的触发时机。
6.1、内存释放的三大步骤(“怎么释放”)
C++ 服务端需要依次执行以下三个系统调用,才能彻底销毁一块共享内存:
| 步骤 | 函数 | 作用 | 备注 |
|---|---|---|---|
| 1 | munmap | 解除当前进程对共享内存的映射 | 调用后本进程不能再访问该内存 |
| 2 | close | 关闭由 shm_open 获得的文件描述符 | 回收进程内的句柄资源 |
| 3 | shm_unlink | 删除共享内存对象的名称 | 类似 unlink 删除文件;当所有进程都解除映射后,OS 才真正回收物理内存 |
重要:
shm_unlink只是删除了共享内存的“名字”。即使调用了它,如果还有其它进程(如 Electron)仍然映射着这块内存,其内容依然有效。只有所有进程都执行了munmap并关闭了引用,操作系统才会回收物理内存。这保证了 Electron 使用期间数据的稳定性。
下面是一个典型的 clear() 方法实现:
#include <sys/mman.h> // munmap, shm_unlink
#include <unistd.h> // close
class SharedMemoryCache {
public:
void clear(const std::string& shm_name) {
// 1. 解除映射
if (mapped_ptr) {
munmap(mapped_ptr, shm_size);
mapped_ptr = nullptr;
}
// 2. 关闭文件描述符
if (shm_fd != -1) {
close(shm_fd);
shm_fd = -1;
}
// 3. 删除共享内存对象
if (!shm_name.empty()) {
shm_unlink(shm_name.c_str());
}
shm_size = 0;
}
private:
void* mapped_ptr = nullptr;
int shm_fd = -1;
size_t shm_size = 0;
};
Electron 侧也要解映射:
在 Node.js 中,使用 @cathodique/mmap-io 等库时,需要显式调用 mmap.unmap(buffer) 并关闭文件描述符。建议监听进程退出事件做兜底清理:
process.on('exit', () => {
if (mmapBuffer) mmap.unmap(mmapBuffer);
if (fd) fs.closeSync(fd);
});
6.2、clear() 的触发方式(“何时调用”)
C++ 服务端的 clear() 不会自动执行,必须由明确的逻辑触发。有三种主要方式:
Electron 主动请求释放(最推荐)
通过 gRPC 暴露 Release 接口,让 Electron 在不再需要某块共享内存时主动调用。
定义 proto:
service PointCloudService {
rpc LoadModel(LoadRequest) returns (LoadResponse);
rpc ReleaseModel(ReleaseRequest) returns (ReleaseResponse);
}
message ReleaseRequest { string shm_name = 1; }
Electron 调用(例如用户关闭点云窗口时):
await window.electronAPI.releaseModel('pointcloud_shm');
C++ 服务实现:
grpc::Status ReleaseModel(grpc::ServerContext*, const ReleaseRequest* req, ReleaseResponse*) {
SharedMemoryCache::instance().clear(req->shm_name());
return grpc::Status::OK;
}
✅ 优点:释放时机精确,资源回收及时,无浪费。
C++ 服务内部自动触发
场景 A:加载新模型时自动替换
在 LoadModel 接口中,如果已有旧共享内存,先 clear() 再创建新的。
if (!current_shm_name.empty()) {
SharedMemoryCache::instance().clear(current_shm_name);
}
// 然后创建新共享内存...
✅ 优点:无需额外 API,适合单模型应用(一次只打开一个点云)。
场景 B:LRU 缓存淘汰
当服务需要同时缓存多个点云时,可设置容量上限,超过后自动清理最久未使用的。
void evictIfNeeded() {
if (cache_.size() > MAX_CACHE_COUNT) {
auto oldest = cache_.begin();
oldest->second->clear();
cache_.erase(oldest);
}
}
✅ 优点:多文档应用(如历史记录)下自动管理内存。
进程退出时自动清理(兜底机制)
在 C++ 服务的析构函数或 atexit 中遍历所有共享内存并调用 clear()。
SharedMemoryCache::~SharedMemoryCache() {
for (auto& entry : all_caches_) {
entry.clear();
}
}
✅ 优点:即使 Electron 忘记调用 Release,正常退出时也能清理。
⚠️ 注意:进程被 kill -9 强制终止时不会执行,但操作系统最终会回收所有内存。
推荐组合策略
在实际项目中,建议采用混合触发,兼顾灵活性与安全性:
| 触发方式 | 使用场景 | 作用 |
|---|---|---|
Electron 主动调用 Release | 用户明确关闭模型、切换文件 | 主力释放机制,及时回收 |
| 加载新模型时自动替换 | 单模型应用(每次只加载一个点云) | 防止旧数据遗留 |
| 进程退出时清理 | 任何情况 | 兜底,确保无泄漏 |
典型调用链路:
用户点击“关闭点云”
→ React 调用 window.electronAPI.closeModel()
→ Electron 主进程通过 gRPC 调用 C++ 服务的 ReleaseModel
→ C++ 服务端执行 SharedMemoryCache::clear(shm_name)
→ munmap → close → shm_unlink
→ 共享内存被彻底销毁
通过清晰的释放操作和完善的触发机制,gRPC + mmap 方案既能提供高性能零拷贝数据共享,又能保证内存资源被安全、及时地回收。
七、总结与展望
在 Electron + Three.js 的大点云渲染场景中,将计算下沉到 C++,并实现零拷贝数据共享是性能突破的关键。本文提供的两种方案各有千秋:
- N-API 同进程方案:适合 1000 万点以下,追求极致简单和低延迟的项目。
- gRPC+mmap 跨进程方案:适合超大规模、要求进程隔离、支持后台队列的专业级应用。
未来,随着 WebGPU 的成熟和 SharedArrayBuffer 的普及,我们甚至可以直接在 C++ 中操作 GPU 缓冲区,进一步降低 CPU 开销。但就目前而言,这套混合架构已经能在普通消费级电脑上流畅渲染 5000 万点云。
如果你正在开发类似的桌面三维工具,希望这篇文章能帮你少走弯路。欢迎在评论区交流你的实践心得!
参考资料:
- Node-API 官方文档
- gRPC Node.js 快速开始
- Three.js 使用外部 ArrayBuffer 作为 BufferAttribute
- Boost.Interprocess 共享内存教程
如果你觉得这篇文章有帮助,欢迎点赞收藏👍 有问题可以在评论区交流!
作者:红波 | 专注智驾、机器人标注工具与可视化开发 | 技术栈:TS/Vue/WebGPU/WebGL/ThreeJS/Go/Rust