从10FPS到满帧, 大型3D 场景优化

二喵和圣子把PC端3D星图应用《Cocosmos》移植到鸿蒙和安卓平台时,遭遇了性能灾难——帧率从60FPS暴跌至个位数,画面卡顿几乎无法使用。

论坛再发下优化指南和FAQ包括几个核心优化点.

面对7942个星体实例、数万条连线和复杂天文计算,移动端不堪重负。这就需要AI配合从■■度进行精准优化。

  • 渲染优化
  • CPU计算
  • 内存管理等

二喵讲分享下如何分享下使用Agent来帮助优化经验和系统性优化,最终实现了从8FPS60FPS的性能飞跃,GFX纹理内存从305MB压缩到65MB(-78.7%),且视觉效果基本不变。


优化前

优化后

Agent创建

创建自己的AI Agent, 由多个MD文档组成, 包括了Agent 的System Prompt和Agent 的优化案例;

你是一位精通 Cocos Creator 3.8 的高级性能优化专家。擅长使用各类 Profiler 工具进行性能分析和优化代码,重点关注:

## 核心优化目标
1. 减少函数调用开销 - 合并频繁调用的函数,减少函数调用层级
2. 降低运算复杂度 - 简化算法,减少不必要的计算
3. 最大化对象复用 - 使用对象池,避免频繁创建销毁对象

## 具体优化要求

### 1. 函数调用优化
- 将高频调用的小函数内联化
- 减少 update/lateUpdate 中的函数调用
- 合并相似功能的函数
- 避免深层嵌套调用
- 缓存函数引用,避免重复查找

### 2. 运算优化
- 缓存计算结果,避免重复计算
- 将循环外可提取的运算提到外部
- 使用位运算代替数学运算(适当情况下)
- 减少三角函数、开方等高开销运算
- 使用查表法代替复杂计算
- 避免在循环中进行对象创建

### 3. 对象复用优化
- 实现对象池机制(NodePool、自定义对象池)
- 复用 Vec2、Vec3、Color 等临时对象
- 使用 `out` 参数避免创建新对象
- 预分配数组和对象
- 复用事件对象

### 4. 内存优化
- 及时释放不用的资源
- 避免内存泄漏(移除事件监听、清理定时器)
- 使用弱引用避免循环引用

## 输出要求
1. 提供优化后的完整代码
2. 标注关键优化点并说明优化原因
3. 给出性能提升预估(如减少X%函数调用、降低Y%运算量)
4. 如有必要,提供优化前后的对比说明

## Profiler 性能分析指南

### A. DevTools Performance Profiler 分析

#### 关键指标分析:
1. Main Thread(主线程)分析
   - 识别长任务(Long Tasks)> 50ms
   - 找出 Scripting(脚本执行)时间占比高的函数
   - 分析 Update/LateUpdate/tick 的调用频率和耗时

2. Call Tree分析
   - 按 Self Time 排序,找出最耗时的函数
   - 按 Total Time 排序,找出调用链中的瓶颈
   - 识别 Hot Functions(热点函数)

3. Bottom-tree分析
   - 查看哪些底层函数被频繁调用
   - 分析函数调用来源
   - 优化高频调用的叶子节点函数

通过优化案例(比较长,只列部分)和上下文管理, 能让Agent 找到更好的底层方法/API来提升性能.

优化流程

通过引擎Profiler/Chrome Profiler/第三方Profiler先定位热点函数, 再重点优化和分析.

Agent优化分析Profiler

大部分的优化都是配合Agent, 这里就不赘述和放具体截图.

Agent分析的结果:
这个 Chrome Profiler 的数据分析,您的 Cocos 3.8 重度3D项目存在以下主要卡顿点:

:red_circle: 严重性能瓶颈

  1. updateWorldTransform (779.3ms, 19.8%)
    问题: 世界坐标变换更新占用最高
    原因: 场景节点层级过深或节点数量过多
    优化建议:
    减少场景节点数量,合并静态物体
    避免深层嵌套的节点结构
    使用对象池复用节点
    冻结不需要移动的节点变换
  2. Profiling overhead (557.6ms, 14.2%)
    问题: 性能分析本身的开销过大
    建议: 这是 profiler 自身消耗,实际发布版本不会有
  3. dispatch (1,423.7ms, 36.2%)
    问题: 事件分发系统消耗巨大
    优化建议:
    减少事件监听器数量
    及时移除不需要的事件监听
    避免在每帧触发大量事件
    使用事件委托模式

:yellow_circle: 次要性能问题

  1. pyramidCulling (371.9ms, 9.5%)
    问题: 视锥剔除计算耗时
    优化建议:
    优化场景分块
    使用更高效的遮挡剔除算法
    减少需要剔除判断的对象数量
  2. (anonymous) 函数调用 (1,299.9ms, 33.1%)
    问题: 大量匿名函数调用
    优化建议:
    减少闭包和匿名函数使用
    缓存函数引用
    避免在循环中创建函数

:green_circle: 渲染相关优化点

  1. _drawStarsIndicativeLines (195.0ms, 5.0%)
  2. _syncActivating (869.4ms, 22.1%)
    问题: 自定义渲染和同步逻辑耗时
    优化建议:
    使用批处理减少 Draw Call
    优化顶点数据结构
    避免每帧重建几何体

开始优化

整个项目使用了自定义的TimeSystem驱动逻辑更新。为了方便Profiler精准定位性能瓶颈,在优化前对几个关键脚本的tick函数都做了命名标记,确保在性能分析工具中能清晰区分各模块的CPU耗时。

更新节流和时间分区

游戏内存在数千的星球, 需要每帧根据他们的星系和当前观察者为之决定这些星球是否渲染,和他们的Label透明度.

这里做了2个机制

节流

  • 通过分档位控制不常用方法的更新频率,避免每帧都执行
  • 提供多个更新频率档位(1帧/3帧/5帧/7帧/10帧),可根据需求动态配置
  • 可以减少30-40%的函数调用开销
export const UPDATE_INTERVALS = {
    SCALE:1,      // 实时更新:每帧, FPS = 60 时候x2
    SUPER_HIGH:1, // 超高频更新:每1帧,FPS = 60 时候x2 2帧一次
    HIGH: 3,      // 高频更新:每3帧
    MEDIUM: 5,    // 中频更新:每5帧
    LOW: 7,       // 低频更新:每7帧
    SUPER_LOW: 10, // 低频更新:每10帧
} 

protected starOnTick(dt?: time.sec, tick?: num.int): void {
    if (this._placeholder) return;
    this._frameCounter++;
    if (this._frameCounter >=  UPDATE_INTERVALS.SUPER_LOW) {
        this._frameCounter = 0;
        this._syncActivating();
        this._syncInstancing();
        this._syncNameLabel();
    }
}

时间分片(Time Slicing)

  • 虽然近万个实例做了分帧加载,但仍集中在少数几帧内执行
  • 通过随机初始化每个实例的帧计数器,将实例的更新操作分散到不同帧
  • 避免大量实例在同一帧执行 update/tick 方法造成性能尖峰
private _frameCounter =  Math.round(UPDATE_INTERVALS.SUPER_LOW*Math.random());

优化节点RTS运算

从updateWorldTransform结果来看场景里数千个星球参与渲染, 虽然节点有做大量的instancing, 但是整体的RTS开销还是很大的,

由于整个宇宙比较大, 每个太空星系都是个相对的节点管理器, 我在每个星系里scalar做了DirtyFlag 脏标记检查, 避免重复触发RTS

  private _updateScale(exp: number): void {
        const scale = this._handleSpecialScale(10 ** exp);
        // DirtyFlag 检查:只对比一个值即可(统一缩放)
        if (this._lastScale !== scale) {
            this.node.setWorldScale(scale, scale, scale);
            this._lastScale = scale;
        }
    }

简化函数计算

原始代码内使用了大量setRotationFromEuler之类代码, 但是只运算一个元素, 我们可以直接在四元素内只修改部分数据, 降低数据开销.

update(dt: number) {
    // setRotationFromEuler 内部会进行大量无关计算
    // 即使只修改 Y 轴,也会处理 X、Z 轴的计算
    this.node.setRotationFromEuler(0, this._rotateAngle, 0);
}

// 引擎原始实现
setRotationFromEuler(x: number, y: number, z: number) {
    // 问题1: 设置完整的欧拉角(即使 x=0, z=0)
    Vec3.set(this._euler, x, y, z);
    this._eulerDirty = false;
    
    // 问题2: 计算所有三个轴的旋转(大量三角函数调用)
    const halfX = x * halfToRad;
    const halfY = y * halfToRad;
    const halfZ = z * halfToRad;

    const sx = Math.sin(halfX);
    const cx = Math.cos(halfX);
    const sy = Math.sin(halfY);
    const cy = Math.cos(halfY);
    const sz = Math.sin(halfZ);
    const cz = Math.cos(halfZ);
    
    // 问题3: 复杂的四元数计算(涉及所有轴)
    this._lrot.x = sx * cy * cz + cx * sy * sz;
    this._lrot.y = cx * sy * cz - sx * cy * sz;
    this._lrot.z = cx * cy * sz + sx * sy * cz;
    this._lrot.w = cx * cy * cz - sx * sy * sz;
    
    // 问题4: 触发子节点更新
    this.invalidateChildren(TransformBit.ROTATION);
}
//简化版
export function setAngleFromY(node: Node, y: number) {
    // 优化1: 直接设置欧拉角,只修改 Y 值
    Vec3.set(node['_euler'], 0, y, 0);
    node['_eulerDirty'] = false;
    // 优化2: 只计算 Y 轴旋转(减少 4 次三角函数调用)
    const lrot = node['_lrot'];
    const halfY = y * halfToRad;
    // 优化3: 简化的四元数计算(X=0, Z=0 时的特殊情况)
    // 当 x=0, z=0 时: sx=0, cx=1, sz=0, cz=1
    // 简化为: q = (0, sin(halfY), 0, cos(halfY))
    lrot.x = 0;
    lrot.y = sin(halfY);
    lrot.z = 0;
    lrot.w = cos(halfY);
    
    // 优化4: 保持原有的子节点更新逻辑
    node.invalidateChildren(TransformBit.ROTATION);
}

使用Global Uniform减少Shader更新

太阳系数百个星体都是用了自定义的光照系统, 根据发光球体的位置来计算光照方向

  #if CUSTOM_SOLAR_LIGHT
      data.L = -FSInput_worldPos.xyz 
       + solarParams.xyz;
      data.L = normalize(data.L);
  #else
置

在原来实现方法中, 除去剔除后的节点, 每帧也需要同步近百次太阳的位置

 private _sycSolarPos(): void {
        this._sharedPasses.forEach((pass, idx) => pass.setUniform(this._sharedHandles[idx], this._sun.node.worldPosition));
    }
    

我在新管线可以定义一个全局的 solarParams uniform, 指需求全局更新一次即可, 在老版本也可以hack一些没有使用到的全局uniform来实现.

多个Uniform合并

原始代码中有大量单float的uniform如

this._pass0.setUniform(this._handlePow10, pow10);
this._pass0.setUniform(this._handleRatio, this._ratio);
        

我们可以把这些uniform 合并成一个vec4 或者更大的数组.

内联计算减少函数调用开销

通过将向量运算(如 Vec3.subtract、Vec3.dot、lengthSqr)展开为直接的数学计算,消除了热点路径中的函数调用开销和临时对象分配。这种内联优化在高频执行的 update 循环中能显著降低 CPU 开销,同时利用提前距离检查(sqDis > 40000)实现快速剔除,进一步提升了剔除算法的执行效率。

关键改进点:

  • 避免大量工具函数的调用栈开销
  • 减少中间临时变量(如 _v3a)的内存分配
  • 采用平方距离比较避免昂贵的 sqrt 运算
    // const _forward = camera._forward;
    // const _pos = camera.node._pos;
    // const forward = camera.forwardDir;
    // const _pos = camera.node._pos;

    // Vec3.subtract(_v3a, target._pos, _pos);
    // if (Vec3.dot(_v3a, forward) < (-allowance)) return true;
    // const dis = _v3a.length()
    // return false
    // const { right, up } = camera.node;
    // return dis >200 ? true : false;
    //优化后
    const fwd = camera.forwardDir;
    const cp = camera.node._pos;
    const tp = target._pos;

    const dx = tp.x - cp.x;
    const dy = tp.y - cp.y;
    const dz = tp.z - cp.z;

    // 先做廉价的距离检查
    const sqDis = dx * dx + dy * dy + dz * dz;
    if (sqDis > 40000) return true;

    // 然后做点积检查
    if (dx * fwd.x + dy * fwd.y + dz * fwd.z < -allowance) return true;
    return false
    

自定义Shader减少编译时间

参考轻量化光照Shader实现,通过剔除非必需的Shader Chunk和条件分支(Switch/Define),显著降低着色器编译时间和变体数量。

img-blackhole

仅保留项目实际使用的光照模型、特性开关和渲染路径,避免引擎默认Shader中大量冗余的通用功能模块,从而加快首次加载速度并减少运行时Shader编译卡顿。

改进点:

  • 结合项目场景, 优化Shader内BRDF计算
  • 移除未使用的光照计算模块(如多光源、阴影、反射、天空盒等)
  • 减少预处理宏定义和条件编译分支,降低排列组合产生的变体数

机型适配

通过设备GPU性能分级(如Mali-G52/Adreno 618等入门级、Mali-G78/Adreno 730等中高端),动态调整渲染参数以适配不同硬件能力。低端设备采用更激进的剔除距离(如200000 lengthSqa)、降低目标帧率(30fps vs 60fps)、启用更激进降帧策略,确保游戏在各档设备上都能维持流畅体验,避免低端机卡顿和高端机性能浪费。

关键改进点:

  • 建立GPU设备白名单/黑名单映射表,启动时自动识别档位
  • 分级配置剔除距离、geo、粒子数量等渲染参数
  • 实现自适应降帧机制
public static SegmentProfiles = {
    // 低性能档位 - 移动端低端设备
    LOW: {
        CIRCLE: 16,          // 圆环段数 (降低 65%)
        ELLIPSE: 64,         // 椭圆段数 (降低 75%)
        CYLINDER: 8,         // 圆柱段数 (降低 50%)
        LINE: 4,             // 渐线段数 (降低 33%)
        ARC: 24,             // 圆弧段数 (降低 62%)
    },
    
    // 中等性能档位 - 移动端中端设备/PC集显
    MEDIUM: {
        CIRCLE: 24,          // 圆环段数 (降低 48%)
        ELLIPSE: 128,        // 椭圆段数 (降低 50%)
        CYLINDER: 12,        // 圆柱段数 (降低 25%)
        LINE: 5,             // 渐线段数 (降低 17%)
        ARC: 32,             // 圆弧段数 (降低 50%)
    },
    
    // 高性能档位 - PC独显/移动端旗舰
    HIGH: {
        CIRCLE: 32,          // 圆环段数 (降低 30%)
        ELLIPSE: 192,        // 椭圆段数 (降低 25%)
        CYLINDER: 16,        // 圆柱段数 (保持原值)
        LINE: 6,             // 渐线段数 (保持原值)
        ARC: 48,             // 圆弧段数 (降低 25%)
    },
    
    // 超高性能档位 - 编辑器/开发环境
    ULTRA: {
        CIRCLE: 46,          // 圆环段数 (原始值)
        ELLIPSE: 256,        // 椭圆段数 (原始值)
        CYLINDER: 16,        // 圆柱段数 (原始值)
        LINE: 6,             // 渐线段数 (原始值)
        ARC: 64,             // 圆弧段数 (原始值)
    }
};

// 当前激活的段数配置
public static Segments = GizmoConfig.SegmentProfiles.ULTRA;

// 根据GPU档位动态设置
public static setQualityLevel(level: 'LOW' | 'MEDIUM' | 'HIGH' | 'ULTRA') {
    this.Segments = this.SegmentProfiles[level];
    console.log(`[Gizmo] 切换至 ${level} 档位,圆环段数: ${this.Segments.CIRCLE}`);
}

如通过GPU分级(GPU一般都和CPU关联),如在手机端使用中低档的线段数量, 在低端机上绘制更简单的几何渲染器, 并没有太影响视觉.

纹理压缩

针对移动端GPU内存限制,构建独立的移动端资源Bundle,将纹理分辨率从PC端的1024-2048px降档至256-1024px,并启用ARM平台原生支持的ASTC纹理压缩格式,在保证视觉质量的前提下,显存占用降低70-80%。配合模型减面优化,综合降低GPU带宽压力和渲染开销。

  • UI纹理: ASTC 5x5
  • 图片纹理: ASTC 6x6

移动端纹理压缩后:

LOD策略

基于原有的LOD策略, 在基础上针对手机做了更激进的面数和Shader 优化, 再保证画质的情况下, 只有近距离观察星体时候始终最高档.

引擎源码优化

通过Profiler定位到性能开销最大的Component后,将其源码和性能数据喂给AI(推荐Claude 4.5),让AI分析瓶颈并给出优化建议。但这种方式需要对引擎底层架构非常熟悉, 大部分修改都不能直接用, 比如AI优化完GeometryRenderer自己又修改了很久才能跑起来.

测试

目前很多工具类似鸿蒙的DevEco也提供了AI测试功能, 我们可以看到优化后整体的CPU Usage 和 GPU Usage 都比较平稳了,

FAQ

也可以关注我公众号 老菜喵 反馈

  1. 性能提升的优先级
  • 分帧>高中低机型设置>算法精简>分时区>去掉每帧uniform设置>shader精简
  1. 设置 game.frameRate =30 后感觉特别卡
  • 主要是触摸上的操作, 可以把移动的距离写在update里和dt关联, 同时加上lerp
  1. 3D游戏 game.frameRate =30 时候, 感觉2D血条/文字 convertToUINode 跟不上节点,
  • 在fps = 60 时候也跟不上, 因为3D相机 update矩阵在2D和批后面, 只是刷新频率快 ; 可以在设置2D节点位置前, 手动刷新3D相机的矩阵;
    image
    update后高速移动截屏还是能跟上
  1. 引擎JS修改后原生平台无效
  • 原生平台大部分模块都在CPP, 好处是可以直接修改无需引擎编译, 可以把JS优化的内容和CPP丢给GPT
  1. Chrome优化完热点后, 在手机提升不大, 同上, 可以观察下 手机上开启debug 后链接chrome调试

  2. Shader优化精简后提升不大

  • 手机端跑PBR大部分不会有性能问题, 主要减少变体, 提升编译速度
33赞

太牛逼了,学习

UI渲染优化 3.x

1赞

膜拜,学习

太厉害了,完全是接触不到的东东!大佬NB

游戏本身看上去更是硬核啊 :joy:

膜拜,学习

看不懂,但感觉很厉害的样子

用了ASTC,物理内存包是不是变大了

本年度论坛最硬核文章

太牛了!!

1赞

感谢支持…

继续分享些邪修的

当场景中大量使用BmFont时候, 经常会发现 fillMeshVertices3D 方法占比很高

正常 :confused:

修改引擎代码, 展开颜色, 分正交和透视, 大部分情况下不需要运算 透视

export function fillMeshVertices3D (node: Node, renderer: IBatcher, renderData: RenderData | null, color: Color): void {
    if (!renderData) return;
    const chunk = renderData.chunk;
    const dataList = renderData.data;
    const vData = chunk.vb;
    const vertexCount = renderData.vertexCount;
    const m = node.worldMatrix;

    const m00 = m.m00; const m01 = m.m01; const m02 = m.m02; const m03 = m.m03;
    const m04 = m.m04; const m05 = m.m05; const m06 = m.m06; const m07 = m.m07;
    const m12 = m.m12; const m13 = m.m13; const m14 = m.m14; const m15 = m.m15;

    // 预计算颜色,避免每次循环都转换
    const colR = color.r / 255;
    const colG = color.g / 255;
    const colB = color.b / 255;
    const colA = color.a / 255;

    // 检查是否需要透视除法(优化常见情况)
    const needsPerspective = m03 !== 0 || m07 !== 0 || m15 !== 1;

    let vertexOffset = 0;

    if (needsPerspective) {
        // 透视投影路径
        for (let i = 0; i < vertexCount; ++i) {
            const vert = dataList[i];
            const x = vert.x;
            const y = vert.y;
            
            let rhw = m03 * x + m07 * y + m15;
            // 优化:避免条件判断,使用数学技巧
            // 如果 rhw 接近 0,设为一个很小的数避免除零
            rhw = 1 / (rhw + (rhw === 0 ? 1e-10 : 0));
            
            vData[vertexOffset] = (m00 * x + m04 * y + m12) * rhw;
            vData[vertexOffset + 1] = (m01 * x + m05 * y + m13) * rhw;
            vData[vertexOffset + 2] = (m02 * x + m06 * y + m14) * rhw;
            vData[vertexOffset + 5] = colR;
            vData[vertexOffset + 6] = colG;
            vData[vertexOffset + 7] = colB;
            vData[vertexOffset + 8] = colA;
            
            vertexOffset += renderData.floatStride;
        }
    } else {
        // 正交投影快速路径(大多数 2D 情况)
        for (let i = 0; i < vertexCount; ++i) {
            const vert = dataList[i];
            const x = vert.x;
            const y = vert.y;
            
            vData[vertexOffset] = m00 * x + m04 * y + m12;
            vData[vertexOffset + 1] = m01 * x + m05 * y + m13;
            vData[vertexOffset + 2] = m02 * x + m06 * y + m14;
            vData[vertexOffset + 5] = colR;
            vData[vertexOffset + 6] = colG;
            vData[vertexOffset + 7] = colB;
            vData[vertexOffset + 8] = colA;
            
            vertexOffset += renderData.floatStride;
        }
    }

    // fill index data
    const vid = chunk.vertexOffset;
    const meshBuffer = chunk.meshBuffer;
    const ib = meshBuffer.iData;
    let indexOffset = meshBuffer.indexOffset;
    
    // 优化索引填充(展开循环)
    const quadCount = vertexCount >> 2; // 除以4
    for (let i = 0; i < quadCount; i++) {
        const start = vid + (i << 2); // i * 4
        ib[indexOffset] = start;
        ib[indexOffset + 1] = start + 1;
        ib[indexOffset + 2] = start + 2;
        ib[indexOffset + 3] = start + 1;
        ib[indexOffset + 4] = start + 3;
        ib[indexOffset + 5] = start + 2;
        indexOffset += 6;
    }
    
    meshBuffer.indexOffset += renderData.indexCount;
    meshBuffer.setDirty();
}

邪修 :smiling_imp:

合并函数, 透视, Z轴统统不考虑! 不对Color A 做Float32转换, 直接用node._uiProps.opacity, 更偏激的点 我们可以把rgb Color, 直接用ArrayBuffer 缓存 Float32的RGBA

   fillBuffers(comp: Label, renderer: IBatcher): void {
        const node = comp.node;
        // 临时颜色,保持与原来逻辑(color.a 使用 node._uiProps.opacity)

        const renderData = comp.renderData;
        if (!renderData) return;

        const chunk = renderData.chunk;
        const dataList = renderData.data;
        const vData = chunk.vb; // 顶点缓冲 Float32Array
        const vertexCount = renderData.vertexCount;
        const floatStride = renderData.floatStride; // 顶点步长(float 数)
        const m = node.worldMatrix;

        // 缓存世界矩阵分量(只需要正交变换分量)
        const m00 = m.m00; const m01 = m.m01; 
        const m04 = m.m04; const m05 = m.m05;
        const m12 = m.m12; const m13 = m.m13; 
        // 对于 2D 正交路径,m03,m07,m15 不参与透视除法(如果你的节点有 3D 变换请谨慎)
        // 这里不做透视,直接使用仿射变换 (x,y,1) * matrix
        const color = comp.color;
        // 预计算颜色(归一化到 0..1,写入顶点颜色槽)
        const colR = color.r / 255;
        const colG = color.g / 255;
        const colB = color.b / 255;
        const colA = node._uiProps.opacity;

        // 顶点写入起始偏移
        let vertexOffset = 0;

        // 为性能,把常用量缓存为局部变量引用
        const list = dataList;
        const stride = floatStride;

        // 主循环:遍历每个顶点并写入位置与颜色
        for (let i = 0; i < vertexCount; ++i) {
            const vert = list[i];
            const x = vert.x;
            const y = vert.y;
            // 写入变换后的位置(仿射 2D 变换)
            vData[vertexOffset] = m00 * x + m04 * y + m12; // x'
            vData[vertexOffset + 1] = m01 * x + m05 * y + m13; // y'
            // vData[vertexOffset + 2] = m02 * x + m06 * y + m14; // z' (如果不使用可忽略)
            // 保持原先顶点布局中 u,v 或其他槽位(如果需要写 uv,请在此处写入)
            // 例如原版可能在 offset 3,4 写入 uv,这里保持不变(如果 renderData 已经填好了 uv,这里可以跳过)
            // 写入颜色到假定的偏移 +5..+8(与原代码一致)
            vData[vertexOffset + 5] = colR;
            vData[vertexOffset + 6] = colG;
            vData[vertexOffset + 7] = colB;
            vData[vertexOffset + 8] = colA;

            vertexOffset += stride;
        }

        // 填充索引
        const vid = chunk.vertexOffset; // 顶点基址(vertex index 起始值)
        const meshBuffer = chunk.meshBuffer;
        const ib = meshBuffer.iData; // Uint16Array 或 Uint32Array
        let indexOffset = meshBuffer.indexOffset;

        // quadCount = vertexCount / 4
        const quadCount = vertexCount >> 2;
        for (let q = 0; q < quadCount; ++q) {
            const start = vid + (q << 2); // q * 4
            ib[indexOffset] = start;
            ib[indexOffset + 1] = start + 1;
            ib[indexOffset + 2] = start + 2;
            ib[indexOffset + 3] = start + 1;
            ib[indexOffset + 4] = start + 3;
            ib[indexOffset + 5] = start + 2;
            indexOffset += 6;
        }

        // 更新 meshBuffer 状态
        meshBuffer.indexOffset += renderData.indexCount;
        meshBuffer.setDirty();
    }
2赞

给大佬顶一个

我理解是包体吧, 通常情况下 PNG8(TinyPNG) 压缩后是比 ASTC小; 如果目标是减少包体 对于 UI、图标、带大量透明区域的素材,优先用 PNG/PNG8的 atlas;对场景贴图、法线贴图、位图照片类用 ASTC。

niuBi

大佬六百六十六

真的是应用AI去落地解决游戏问题了,而不是虚有其表的去讲AI

老板看到了会怎么想

鸿蒙的DevEco 可以直接ai测试ccc吗还是说要生成指定包来测试

用AI也是需要技术的