diff --git a/cocos/3d/assets/mesh.ts b/cocos/3d/assets/mesh.ts index c86e301912..7a3983bc88 100644 --- a/cocos/3d/assets/mesh.ts +++ b/cocos/3d/assets/mesh.ts @@ -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'; @@ -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); } @@ -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); diff --git a/cocos/primitive/define.ts b/cocos/primitive/define.ts index 5c2e11ffe1..0f3f07d70f 100644 --- a/cocos/primitive/define.ts +++ b/cocos/primitive/define.ts @@ -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. @@ -239,7 +257,7 @@ export interface IDynamicGeometry { */ customAttributes?: { attr: Attribute, - values: Float32Array, + values: DynamicAttributeValues, }[]; /** diff --git a/native/cocos/3d/assets/Mesh.cpp b/native/cocos/3d/assets/Mesh.cpp index f3448109f4..a5f4e8a9ee 100644 --- a/native/cocos/3d/assets/Mesh.cpp +++ b/native/cocos/3d/assets/Mesh.cpp @@ -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" @@ -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; @@ -1199,30 +1209,30 @@ void Mesh::updateSubMesh(index_t primitiveIndex, const IDynamicGeometry &geometr return; } - ccstd::vector buffers; + ccstd::vector buffers; if (!geometry.positions.empty()) { - buffers.push_back(&geometry.positions); + buffers.push_back(wrapAsTypedArray(geometry.positions)); } 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); } } @@ -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(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; diff --git a/native/cocos/primitive/PrimitiveDefine.h b/native/cocos/primitive/PrimitiveDefine.h index 1e8aa0ef3c..fdc07f2878 100644 --- a/native/cocos/primitive/PrimitiveDefine.h +++ b/native/cocos/primitive/PrimitiveDefine.h @@ -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; }; /**