# Rendering **Repository Path**: bo1/rendering ## Basic Information - **Project Name**: Rendering - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-26 - **Last Updated**: 2026-08-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Cocos Creator 3D 蒙皮模型轮廓实验 本项目使用 Cocos Creator 3.8.8,以 PA Shark 骨骼动画模型为例,对比两种屏幕空间轮廓提取方式: - CPU 蒙皮、投影和几何轮廓边分类,使用红点显示结果。 - GPU 离屏 RenderTexture Alpha Mask、膨胀和点阵 Shader,使用青点显示结果。 当前代码同时显示两种结果,目的是验证算法。正式功能只需要轮廓坐标时,应关闭 `Graphics` 红点和轮廓 `Sprite` 等调试绘制。 ## 当前实现 主要文件: - `assets/3D/script/scene3D.ts`:模型加载、动画启动、CPU 轮廓计算和 GPU 离屏 Mask 管理。 - `assets/3D/script/skinningMath.ts`:蒙皮、顶点焊接、三角形朝向和轮廓边判定等纯数学逻辑。 - `assets/resources/effects/offscreen-outline-points.effect`:Alpha 膨胀、外轮廓提取和点阵显示 Shader。 - `tools/tests/skinningMath.test.ts`:拓扑与轮廓数学测试。 鲨鱼预制体实例化后会执行以下设置: 1. 开启 `SkeletalAnimation.useBakedAnimation`。 2. 播放 `Take 001` 动画。 3. 模型缩放设置为 `1500`,绕 Y 轴旋转 90 度,并挂到 `modeRoot`。 4. 延迟 2 秒,等待烘焙动画模型及关节纹理元数据可用,然后初始化两种轮廓算法。 ## 方案一:CPU 蒙皮与几何轮廓红点 ### 原理 初始化阶段从每个 `SkinnedMeshRenderer` 读取以下数据: - 顶点位置 `POSITION`; - 最多四个骨骼索引 `JOINTS`; - 对应蒙皮权重 `WEIGHTS`; - 三角形索引; - `Skeleton.bindposes` 中的逆绑定姿态矩阵。 模型经常因为 UV、法线或材质边界而把同一几何顶点拆成多个顶点。代码先按照“绑定姿态位置 + 有效骨骼索引 + 权重”焊接等价顶点,再建立边到相邻三角形的拓扑关系。这一步只在初始化时执行。 每帧的处理流程如下: 1. 优先读取预烘焙动画当前帧的骨骼姿态;读取不到时,退回到骨骼节点的实时世界变换。 2. 使用线性混合蒙皮计算动画后的局部坐标: `p_skin = Σ weight[i] × (pose[i] × inverseBindPose[i]) × p_bind` 3. 再经过模型世界矩阵、摄像机 View/Projection 和视口映射得到屏幕坐标: `p_screen = viewport(Projection × View × Model × p_skin)` 4. 根据屏幕空间有符号面积,把每个三角形分类为正面、背面或退化三角形。 5. 若一条边只有一个有效相邻三角形,或者它连接的相邻三角形在屏幕上朝向相反,则把它认定为几何轮廓候选边。 6. 沿候选边按照 `vertexPointSpacing` 采样,并由 `Graphics` 绘制红点。 ### 优点 - 轮廓点直接位于 CPU 内存中,可以在当前帧继续用于碰撞提示、UI 锚点、路径分析或其他逻辑。 - 不需要从 GPU 回读 RenderTexture,避免同步 `readPixels` 带来的 CPU/GPU 流水线停顿。 - 不替换模型材质、Pass、宏或渲染状态,因此不会直接拆散原摄像机下已经成立的材质合批。 - 拓扑和顶点焊接结果可缓存;轮廓精度不依赖 RenderTexture 分辨率。 - 可对单个怪物分别保留轮廓点及对象标识,不会天然合并成一张联合 Mask。 ### 缺点 - 每帧需要执行骨骼矩阵更新、四权重顶点蒙皮、世界/摄像机投影、三角形分类和边采样;怪物数量增加时,计算量近似线性增长。 - 当前代码读取 `BakedSkinningModel` 的内部烘焙数据,这不是稳定的公共 API,升级 Creator 版本时需要重新核对。 - 当前红点是“几何轮廓候选”,不是最终像素可见轮廓;它没有深度缓冲可见性测试。 - 材质透明、Alpha Test、溶解、顶点 Shader 位移等视觉变化不会自然反映到 CPU 几何轮廓中。 - 顶点焊接只在一个 Primitive 内完成,不同材质 Primitive 之间的接缝目前不能互相消除。 - 当前裁剪策略要求三角形三个顶点的深度都在 `[0, 1]`,没有对穿越近裁剪面或远裁剪面的边做严格裁剪。 ## 方案二:GPU 离屏 Alpha Mask 与青色轮廓点 ### 原理 代码为鲨鱼寻找一个未使用的用户 Layer,并创建一台与主 3D 摄像机位置、旋转、投影方式、裁剪面和视口一致的离屏摄像机。该摄像机只渲染 Mask Layer,输出到最大边长不超过 2048 的 `RenderTexture`。 当前离屏摄像机仍使用模型原有材质渲染,因此骨骼蒙皮、三角形裁剪、深度测试和材质 Alpha 都由 GPU 正常处理。RenderTexture 随后显示在全屏 `Sprite` 上,轮廓 Shader 对每个像素执行: 1. 读取中心 Alpha,并在上下、左右及四个对角方向额外采样,共计 9 次纹理采样。 2. 对 Alpha 做近似膨胀。 3. 使用 `outerEdge = dilatedAlpha - centerAlpha` 只保留 Mask 外侧的一圈像素。 4. 使用固定屏幕点阵和圆形距离场,把连续外圈裁成青色点。 这个过程完全停留在 GPU,当前实现没有把 RenderTexture 像素回读到 CPU。 ### 优点 - 骨骼蒙皮、深度测试和遮挡关系都复用 GPU 渲染结果,更接近玩家最终看到的像素轮廓。 - 对复杂网格不需要在 JavaScript 中逐顶点、逐三角形执行蒙皮和分类,CPU 开销较稳定。 - 同一 Mask Layer 中的多个怪物可以由一台共享摄像机一次生成联合 Mask,重叠怪物之间能够使用同一深度缓冲处理前后关系。 - Alpha Test、透明贴图或其他已经写入离屏 Alpha 的材质形状,可以自然参与轮廓计算。 ### 缺点 - 模型会被离屏摄像机再提交和渲染一次;这不会修改原摄像机的合批结果,但会增加一个渲染阶段及对应 Draw Call、蒙皮和像素着色开销。 - 全屏轮廓 Pass 每个像素约进行 9 次 Mask 采样,成本与 RenderTexture 分辨率和屏幕覆盖无关;低端设备应控制分辨率和更新频率。 - 结果是一张纹理,不是 CPU 坐标。同步回读可以在同帧得到数据,但通常会阻塞渲染流水线,不适合作为低端机每帧方案。 - 当前 Mask 只包含指定 Layer。其他 Layer 的场景物体不会写入这张深度缓冲,因此青色轮廓可能透过未参与 Mask Pass 的前景遮挡物。 - 多个怪物写进同一 Alpha Mask 后得到的是“所有怪物并集的外轮廓”。若需要每只怪物独立轮廓,仅有 Alpha 不足以区分归属。 - 当前算法只取固定半径的 8 邻域最大值,是低成本近似膨胀,不是严格的多次形态学膨胀;大轮廓宽度下可能出现形状误差。 ## 为什么红点和青点差别较大 两种颜色表达的对象不同: - 红点表达拓扑和三角形朝向推导出的几何候选轮廓。 - 青点表达当前离屏 Mask 中最终可见 Alpha 的外边界。 差异主要来自以下因素: 1. 红点没有深度可见性过滤。鲨鱼背面、自遮挡区域或被另一只怪物挡住的候选边仍可能保留下来。 2. GPU Mask 使用真正的光栅化、深度测试、背面剔除和材质 Alpha,CPU 当前实现只处理网格几何。 3. CPU 只能在同一个 Primitive 内焊接顶点;材质分片边界可能被误认为开放边。 4. 红点沿线段按距离采样,青点使用固定屏幕网格。即使轮廓位置相同,点的相位和间距也不会完全重合。 5. GPU 轮廓受 RenderTexture 分辨率、Alpha 阈值和膨胀采样方向影响;CPU 轮廓则是连续屏幕坐标。 6. 多怪物共用一张 Mask 时,青点显示并集轮廓;CPU 默认仍分别处理每个 Renderer,因此会保留互相重叠区域中的轮廓。 因此,当前红点可用于验证蒙皮、投影和拓扑算法,但不能直接称为“真正可见轮廓”。 ## 对材质合批的影响 ### CPU 方案 CPU 方案只读取网格、骨骼和相机数据,不修改模型材质、Pass、宏定义或渲染 Layer,原 3D 渲染队列的合批条件保持不变。调试阶段的 `Graphics` 会产生自己的 2D 绘制开销,但正式版本只输出坐标时可以完全移除这部分。 CPU 轮廓计算本身不参与 GPU 合批。多个怪物即使在渲染上已经合批,仍需要被 CPU 算法逐实例处理,因为每个实例的世界变换、动画帧或骨骼姿态可能不同。 ### GPU 方案 离屏摄像机不会改变原摄像机已经使用的材质,所以不会直接打断原渲染阶段的合批;但是离屏摄像机会建立自己的渲染队列并再次提交模型。离屏阶段能否继续合批,仍取决于网格、材质、Pass、宏、渲染状态和实例化数据是否兼容。 多怪物场景必须使用“一台共享 Mask 摄像机 + 一张共享 RenderTexture + 一个轮廓后处理 Pass”。如果每个怪物各自创建摄像机、RenderTexture、材质和全屏 Sprite,开销会随怪物数量快速增长,也失去了合批和共享后处理的意义。 共享 Alpha Mask 适合获取所有怪物的联合外轮廓。若要区分每只怪物,可改为共享 ID Mask:为每个实例输出唯一 ID,再根据中心像素与邻域像素的 ID 差异识别边界。但这仍然只在 GPU 上得到像素结果。 ## 正式需求下的方案选择 当前约束是: - 多个怪物; - 已开启材质合批和预烘焙动画; - 不允许为了轮廓破坏原模型合批; - 必须在一帧内得到所有外围轮廓坐标; - 最终不需要 `Graphics` 或 `Sprite` 调试显示。 在这些约束下,推荐继续使用 CPU 路线,并增加可见性过滤。原因是 GPU Alpha Mask 最擅长生成可视纹理,但要在同一帧把大量轮廓坐标交给 JavaScript,只能同步回读或依赖并非所有目标平台都支持的 GPU Compute/存储缓冲区;同步回读在低端设备上通常比经过优化的 CPU 计算更不稳定。 需要明确:无法同时无成本地满足“与最终 GPU 像素完全一致”“同帧获得 CPU 坐标”和“不发生 GPU 回读”。下一阶段应先定义轮廓只考虑怪物之间的遮挡,还是还要考虑场景中墙体、地形和特效的遮挡。 ## 下一步:真正可见红点的推荐实现 ### 第一阶段:共享静态缓存 把当前由单个 `scene3D` 持有的数据提取到轮廓管理器中,并按 `Mesh/Skeleton` 共享: - 原始顶点、骨骼索引和权重; - 焊接后的顶点 ID; - 三角形索引和边邻接; - Primitive 之间可安全合并的几何接缝; - 轮廓点输出缓冲区。 多个相同鲨鱼实例只构建一次拓扑缓存。实例只保留动画帧、世界矩阵和临时投影数据。 ### 第二阶段:降低每帧矩阵开销 当前 3D 和 2D 摄像机都使用正交投影时,可以把每个实例的 `Model × View × Projection × Viewport` 关系预先合成,并进一步与每个骨骼的蒙皮矩阵预乘。顶点循环中直接对四个“骨骼到屏幕”的矩阵做权重混合,避免每个顶点再次执行独立的世界矩阵和 `worldToScreen` 调用。 正交投影可以省去透视除法并简化裁剪,但不会消除骨骼蒙皮和三角形可见性计算;主要优化仍来自矩阵预乘、缓存复用和只在动画帧、相机或实例变换发生变化时更新。 若多个实例使用相同 Mesh、动画 Clip 和烘焙帧,可以共享模型局部空间的蒙皮顶点,再分别应用实例到屏幕的正交变换。不同动画帧的实例不能共享这部分结果。 ### 第三阶段:增加 CPU 可见性缓冲 要让红点成为真正的屏幕可见外轮廓,可在 CPU 上为所有目标怪物建立一张共享的低分辨率深度/对象 ID 缓冲: 1. 把所有怪物的蒙皮顶点投影到统一屏幕坐标。 2. 使用分块或扫描线方式把投影三角形光栅化到低分辨率缓冲,记录最近深度和怪物 ID。 3. 对几何候选轮廓边进行屏幕空间采样,并插值采样点深度。 4. 只有当采样点深度与缓冲中的最近深度在容差内,并且邻域存在背景或不同对象 ID 时,才输出这个点。 5. 使用屏幕空间哈希去重,再根据用途选择保留无序点集,或连接成轮廓折线。 这会过滤自遮挡、怪物互相遮挡和联合 Mask 内部的边,同时仍能在同一帧直接得到 CPU 坐标。缓冲分辨率应由允许的坐标误差决定,例如先从屏幕的 `1/2` 或 `1/4` 尺寸开始评估,而不是默认使用全分辨率。 若轮廓只要求“所有怪物并集的最外圈”,也可以直接从 CPU 占用 Mask 上执行边界提取或 Marching Squares;若要求每只怪物独立可见部分,则必须保留对象 ID。 ### 第四阶段:性能与正确性验收 至少覆盖以下场景: - 单只鲨鱼的正面、侧面、近裁剪面和屏幕边缘。 - 两只鲨鱼相交、前后遮挡、使用相同动画帧和不同动画帧。 - 材质被合批时,启用和关闭轮廓计算的原 3D Draw Call/Batch 数不发生变化。 - 关闭调试绘制后,每帧只产生坐标数据,不创建临时数组、节点、材质或纹理。 - 在目标低端设备上记录顶点蒙皮、CPU 深度缓冲、轮廓过滤各阶段耗时,并设置明确的帧预算。 - 以固定分辨率 GPU Mask 为参考,允许 1~2 像素量化误差时,CPU 可见轮廓不再出现明显的内部边或遮挡边。 ## 如果允许延迟一到两帧 若业务允许轮廓坐标比画面晚一到两帧,GPU 方案可以改成共享 ID Mask,并使用双缓冲或三缓冲异步回读。这样能获得更接近最终像素的轮廓,同时减轻同步等待,但结果不再属于严格的当前帧。 在 WebGPU 或支持 Compute Shader 与存储缓冲区的原生后端,还可以让 GPU 压缩轮廓像素为坐标列表后再异步读取;WebGL 目标不能把它当作通用方案。 ## 结论 - 只需要屏幕显示轮廓:优先使用共享 GPU Alpha Mask,视觉结果更可靠。 - 必须同帧获得 CPU 轮廓坐标且不能影响原模型合批:使用优化后的 CPU 蒙皮、拓扑候选边和共享深度/ID 可见性缓冲。 - 必须与 GPU 像素结果高度一致,但允许少量帧延迟:使用共享 GPU ID Mask 和异步回读。 当前红点方案适合作为下一阶段 CPU 可见轮廓的基础,但在加入深度/ID 可见性过滤前,它仍然只是几何候选轮廓。