Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cocos/3d/assets/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import { ccclass, serializable } from 'cc.decorator';
import { EDITOR } from 'internal:constants';
import { Asset } from '../../asset/assets/asset';
import { IDynamicGeometry } from '../../primitive/define';
import { IDynamicGeometry, DynamicAttributeValues } from '../../primitive/define';
import { BufferBlob } from '../misc/buffer-blob';
import { Skeleton } from './skeleton';
import { geometry, cclegacy, sys, warnID, Mat4, Quat, Vec3, assertIsTrue, murmurhash2_32_gc, errorID, halfToFloat, v3 } from '../../core';
Expand Down Expand Up @@ -581,7 +581,7 @@ export class Mesh extends Asset {
return;
}

const buffers: Float32Array[] = [];
const buffers: DynamicAttributeValues[] = [];
if (dynamicGeometry.positions.length > 0) {
buffers.push(dynamicGeometry.positions);
}
Comment on lines 581 to 587
Expand Down Expand Up @@ -618,7 +618,24 @@ export class Mesh extends Asset {
for (let index = 0; index < buffers.length; index++) {
const vertices = buffers[index];
const bundle = this._struct.vertexBundles[primitive.vertexBundelIndices[index]];
const attribute = bundle.attributes[0];
const formatInfo = FormatInfos[attribute.format];
const stride = bundle.view.stride;

// Defensive check: the element type of `vertices` must physically match the GPU
// format declared for this attribute (e.g. a Uint16Array for RGBA16UI), otherwise
// the vertex count derived from byteLength/stride would silently be wrong (see the
// historical "a_joints becomes 2x vertices" bug this replaces). Fail fast with a
// clear diagnostic instead of blindly copying misinterpreted data.
assertIsTrue(
vertices.BYTES_PER_ELEMENT * formatInfo.count === formatInfo.size,
`Custom attribute '${attribute.name}': the element byte size of the supplied `
+ `TypedArray (${vertices.BYTES_PER_ELEMENT}) times its component count (${formatInfo.count}) `
+ `does not match the byte size (${formatInfo.size}) of the declared GPU format. `
+ 'Supply a TypedArray whose element type matches the target format (e.g. Uint16Array '
+ 'for RGBA16UI) instead of converting through Float32Array.',
);

const vertexCount = vertices.byteLength / stride;
const updateSize = vertices.byteLength;
const dstBuffer = new Uint8Array(this._data.buffer, bundle.view.offset, updateSize);
Expand Down
20 changes: 19 additions & 1 deletion cocos/primitive/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@

import { PrimitiveMode, Attribute } from '../gfx';

/**
* @en
* The set of concrete TypedArray types accepted for a dynamic mesh's custom attribute values.
* Unlike the standard attributes (positions/normals/uvs/tangents/colors), which are always
* float-based, a custom attribute's physical GPU format (`attr.format`) can be any integer or
* float format (e.g. RGBA16UI for joint indices). Accepting any concrete TypedArray lets callers
* supply data whose element type already matches the target GPU format (fast copy path),
* avoiding an unnecessary and potentially lossy "integer -> float -> integer" round trip.
* Float32Array remains valid for FLOAT-typed formats (backward compatible).
* @zh
* 动态网格定制属性 values 字段接受的具体 TypedArray 类型集合。与始终为 float 的标准属性
* (positions/normals/uvs/tangents/colors)不同,定制属性的物理 GPU 格式(`attr.format`)可以是
* 任意整型或浮点格式(例如 RGBA16UI 用于骨骼关节索引)。接受任意具体 TypedArray 类型,使调用者可以
* 直接传入与目标 GPU 格式匹配的原生类型数组(快速拷贝路径),避免不必要甚至有损的
* “整数 -> 浮点 -> 整数”往返转换。Float32Array 对于 FLOAT 类型格式依然有效(向后兼容)。
*/
export type DynamicAttributeValues = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array;

/**
* @en
* The definition of the parameter for building a primitive geometry.
Expand Down Expand Up @@ -239,7 +257,7 @@ export interface IDynamicGeometry {
*/
customAttributes?: {
attr: Attribute,
values: Float32Array,
values: DynamicAttributeValues,
}[];

/**
Expand Down
61 changes: 48 additions & 13 deletions native/cocos/3d/assets/Mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "3d/assets/Skeleton.h"
#include "3d/misc/BufferBlob.h"
#include "3d/misc/CreateMesh.h"
#include "base/TemplateUtils.h"
#include "base/std/hash/hash.h"
#include "core/DataView.h"
#include "core/assets/RenderingSubMesh.h"
Expand Down Expand Up @@ -1190,6 +1191,15 @@ const gfx::FormatInfo *Mesh::readAttributeFormat(index_t primitiveIndex, const c
return result;
}

namespace {
// Wrap a standard (always-float) dynamic geometry channel into a `TypedArray` view without
// copying, so it can share the same handling path as `customAttributes` below, whose element
// type may be any concrete TypedArray alternative (not necessarily float).
TypedArray wrapAsTypedArray(const Float32Array &arr) {
return arr;
}
} // namespace

void Mesh::updateSubMesh(index_t primitiveIndex, const IDynamicGeometry &geometry) {
if (!_struct.dynamic.has_value()) {
return;
Expand All @@ -1199,30 +1209,30 @@ void Mesh::updateSubMesh(index_t primitiveIndex, const IDynamicGeometry &geometr
return;
}

ccstd::vector<const Float32Array *> buffers;
ccstd::vector<TypedArray> buffers;
if (!geometry.positions.empty()) {
buffers.push_back(&geometry.positions);
buffers.push_back(wrapAsTypedArray(geometry.positions));
}
Comment on lines +1212 to 1215

if (geometry.normals.has_value() && !geometry.normals.value().empty()) {
buffers.push_back(&geometry.normals.value());
buffers.push_back(wrapAsTypedArray(geometry.normals.value()));
}

if (geometry.uvs.has_value() && !geometry.uvs.value().empty()) {
buffers.push_back(&geometry.uvs.value());
buffers.push_back(wrapAsTypedArray(geometry.uvs.value()));
}

if (geometry.tangents.has_value() && !geometry.tangents.value().empty()) {
buffers.push_back(&geometry.tangents.value());
buffers.push_back(wrapAsTypedArray(geometry.tangents.value()));
}

if (geometry.colors.has_value() && !geometry.colors.value().empty()) {
buffers.push_back(&geometry.colors.value());
buffers.push_back(wrapAsTypedArray(geometry.colors.value()));
}

if (geometry.customAttributes.has_value()) {
for (const auto &ca : geometry.customAttributes.value()) {
buffers.push_back(&ca.values);
buffers.push_back(ca.values);
}
}

Expand All @@ -1234,19 +1244,44 @@ void Mesh::updateSubMesh(index_t primitiveIndex, const IDynamicGeometry &geometr

// update _data & buffer
for (auto index = 0U; index < buffers.size(); index++) {
const auto &vertices = *buffers[index];
const auto &vertices = buffers[index];
auto &bundle = _struct.vertexBundles[primitive.vertexBundelIndices[index]];
const auto &attribute = bundle.attributes[0];
const auto &formatInfo = gfx::GFX_FORMAT_INFOS[static_cast<uint32_t>(attribute.format)];
const auto stride = bundle.view.stride;
const auto vertexCount = vertices.byteLength() / stride;
const auto updateSize = vertices.byteLength();
const auto vertexByteLength = getTypedArrayLength(vertices) * getTypedArrayBytesPerElement(vertices);

// Defensive check: the element type of `vertices` must physically match the GPU format
// declared for this attribute (e.g. a Uint16Array for RGBA16UI), otherwise the vertex
// count derived from byteLength()/stride would silently be wrong (see the historical
// "a_joints becomes 2x vertices" bug this replaces). Fail fast with a clear diagnostic
// instead of memcpy-ing misinterpreted data.
// NOTE: keep the asserted condition in a short-named bool (rather than inlining the full
// expression) because CC_ASSERTF stringifies the condition via #cond into a fixed 256-byte
// buffer together with the message below; a long inlined expression can overflow that
// buffer and trip -Werror=format-truncation on some toolchains (e.g. Android NDK clang).
const auto elementBytes = getTypedArrayBytesPerElement(vertices);
const bool formatMatches = elementBytes * formatInfo.count == formatInfo.size;
CC_ASSERTF(formatMatches,
"Attribute '%s': element size %u * count %u != format size %u. Use a "
"TypedArray matching the GPU format (e.g. Uint16Array for RGBA16UI).",
attribute.name.c_str(), elementBytes, formatInfo.count, formatInfo.size);

const auto vertexCount = vertexByteLength / stride;
const auto updateSize = vertexByteLength;
auto *dstBuffer = _data.buffer()->getData() + bundle.view.offset;
const auto *srcBuffer = vertices.buffer()->getData() + vertices.byteOffset();
auto *vertexBuffer = subMesh->getVertexBuffers()[index];
CC_ASSERT_LE(vertexCount, info.maxSubMeshVertices);

if (updateSize > 0U) {
std::memcpy(dstBuffer, srcBuffer, updateSize);
vertexBuffer->update(srcBuffer, updateSize);
ccstd::visit(overloaded{
[&](const auto &typedArray) {
const auto *srcBuffer = typedArray.buffer()->getData() + typedArray.byteOffset();
std::memcpy(dstBuffer, srcBuffer, updateSize);
vertexBuffer->update(srcBuffer, updateSize);
},
[](const ccstd::monostate & /*unused*/) {}},
vertices);
}

bundle.view.count = vertexCount;
Expand Down
20 changes: 19 additions & 1 deletion native/cocos/primitive/PrimitiveDefine.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,25 @@ struct IGeometry {

struct DynamicCustomAttribute {
gfx::Attribute attr;
Float32Array values;
/**
* @en
* The raw vertex data for this custom attribute. Unlike the standard attributes
* (positions/normals/uvs/tangents/colors) which are always float-based, a custom
* attribute's physical GPU format (gfx::Attribute::format) can be any integer or
* float format (e.g. RGBA16UI for joint indices). To avoid forcing every custom
* attribute through a lossy/unnecessary "integer -> float -> integer" round trip,
* `values` accepts any concrete TypedArray variant so callers can supply data whose
* element type already matches the target GPU format (fast memcpy path). Supplying a
* Float32Array still works for FLOAT-typed formats (backward compatible).
* @zh
* 该定制属性的原始顶点数据。与标准属性(positions/normals/uvs/tangents/colors,始终为
* float)不同,定制属性的物理 GPU 格式(gfx::Attribute::format)可以是任意整型或浮点格式
* (例如 RGBA16UI 用于骨骼关节索引)。为了避免强制所有定制属性都经历不必要甚至有损的
* “整数 -> 浮点 -> 整数”往返转换,`values` 支持任意具体的 TypedArray 类型,调用者可以直接传入
* 与目标 GPU 格式匹配的原生类型数组(走快速 memcpy 路径)。传入 Float32Array 对于 FLOAT
* 类型格式仍然有效(向后兼容)。
*/
TypedArray values;
};

/**
Expand Down
Loading