Skip to content

feat: Add wearable and emotes model checks#3370

Open
LautaroPetaccio wants to merge 9 commits intomasterfrom
feat/add-wearable-and-emotes-model-checks
Open

feat: Add wearable and emotes model checks#3370
LautaroPetaccio wants to merge 9 commits intomasterfrom
feat/add-wearable-and-emotes-model-checks

Conversation

@LautaroPetaccio
Copy link
Contributor

@LautaroPetaccio LautaroPetaccio commented Mar 18, 2026

When creators upload wearables or emotes, there is no feedback about whether their GLB files comply with Decentraland's technical requirements. Issues like exceeding triangle limits, oversized textures, or incorrect armature naming are only discovered after submission — during the curation review process. This causes a slow feedback loop: creators submit, wait for review, get rejected, fix, and re-submit.

This PR adds client-side validation that checks the uploaded GLB against the documented spec at upload time and in the item editor, giving creators immediate, actionable feedback. Validation is informational only — it never blocks saving or uploading. Creators can acknowledge warnings and proceed.

How

Validation Engine (src/lib/glbValidation/)

A new module that takes a parsed Three.js GLTF object and runs a battery of checks, returning a flat list of issues. Each issue has a severity (ERROR or WARNING), a machine-readable code, and a translation key with parameters for the UI message.

The module is split into:

File Purpose
types.ts ValidationIssue, ValidationResult, ValidationSeverity
constants.ts All numeric limits from the Decentraland docs, plus getEffectiveTriangleLimit() for the Tris Combiner rule
wearableValidators.ts 8 validator functions for wearable-specific checks
emoteValidators.ts 12 validator functions for emote-specific checks (including props and audio)
index.ts Entry points: validateWearableGLTF(), validateEmoteGLTF(), revalidateWearableForCategory(), checkTriangleCount()

Validators receive the Three.js module as a parameter (via dynamic import('three')) so they can use instanceof checks for Mesh, Bone, Camera, etc. This avoids a static dependency on Three.js.

Triangle Count Validation & Tris Combiner

Triangle limits depend on the wearable's category and which slots it hides. The Tris Combiner rule allows creators to combine triangle budgets when hiding other slots (e.g., a jumpsuit using upper_body and hiding lower_body gets 1500 + 1500 = 3000 triangles).

Because of this dependency, triangle validation behaves differently at each stage:

Context Category known? Hides known? Behavior
Import time (file drop) No No Triangle check skipped entirely. Other validations (dimensions, materials, textures, bones, etc.) still run.
CreateSingleItemModal (category selected) Yes No Checks against the base category limit. If exceeded, shows an error with a hint: "You can increase this limit by hiding other wearable slots in the Item Editor."
Item Editor (category + hides configured) Yes Yes Full Tris Combiner calculation. The combined limit accounts for all hidden slot budgets. Re-runs automatically when the user changes hides or category in the RightPanel.

The getEffectiveTriangleLimit(category, hides) function in constants.ts computes the combined limit: base category budget + sum of each hidden slot's budget.

Integration into the Upload Flow

Single item upload (ImportStep.tsx):

  • processModel() now calls loadAndValidateModel() from getModelData.ts, which loads the GLTF once, determines the item type (emote vs wearable), and runs the validation suite — all in a single WebGL context.
  • Validation issues are passed through ModelDataAcceptedFileProps → reducer state → context, and displayed in the details step via ValidationIssuesPanel.
  • When the user selects a category in the details step, a lightweight checkTriangleCount() runs against the already-computed metrics.triangles (no GLTF reload) and merges the result into the displayed issues.
  • Validation does not block the import or the save. The creator sees the issues but can proceed.

Multi-item upload (CreateAndEditMultipleItemsModal.tsx):

  • After loading each ZIP file, loadAndValidateModel() runs on the model with the category and hides from the wearable config. Files with error-severity issues are rejected with the concatenated error messages shown in the review table.

Item editor (CenterPanel.tsx):

  • A validation status indicator is rendered in the footer next to the scene boundaries (cylinder) button.
  • When an item is selected, the model is fetched from the builder's content storage and validated with the item's category and hides.
  • The indicator shows: a loader while validating, a green checkmark on pass, a red X on errors, or a yellow exclamation on warnings.
  • Clicking the indicator when there are issues opens a modal with the full ValidationIssuesPanel.
  • A tooltip on hover describes the current status.
  • Validation re-runs automatically when the user changes the item's category or hides in the RightPanel.

UI Component (ValidationIssuesPanel)

A collapsible panel that lists all validation issues. Errors (red) are shown before warnings (yellow). The header shows a count and can be toggled open/closed. Styled for dark theme to match the builder's UI. When inside a modal, the list can grow up to 60vh; when inline (details step), it's capped at 200px to avoid pushing form fields down.

Used in three places:

  1. WearableDetails — shown above the form when creating/editing a wearable
  2. EmoteDetails — shown above the form when creating/editing an emote
  3. CenterPanel validation modal — shown when clicking the status indicator in the item editor

State Management

  • validationIssues added to StateData and AcceptedFileProps in the CreateSingleItemModal types
  • SET_VALIDATION_ISSUES action added to the reducer
  • validationIssues exposed through the modal's React context
  • In CreateSingleItemModal, a useMemo merges import-time issues with the category-dependent triangle check, updating whenever the category changes

Translations

All translation keys added under create_single_item_modal.error.glb_validation.*, validation_issues_panel.*, and item_editor.center_panel.validation_* in en.json, es.json, and zh.json. Spanish and Chinese translations use real localized text.


Validation Rules

Wearable Validations

Check Severity Limit Code
Triangle count (with category, no hides) ERROR Base category limit (see below) + hint about Tris Combiner TRIANGLE_COUNT_EXCEEDED
Triangle count (with category + hides) ERROR Base + sum of hidden slot budgets TRIANGLE_COUNT_EXCEEDED
Model dimensions ERROR 2.42m width, 2.42m height, 1.4m depth DIMENSIONS_EXCEEDED
Material count ERROR 2 (standard), 5 (skin) — excludes AvatarSkin_MAT MATERIALS_EXCEEDED
Texture count ERROR 2 (standard), 5 (skin) — excludes AvatarSkin_MAT TEXTURES_EXCEEDED
Texture resolution ERROR 512x512 px (standard), 256x256 px (facial: eyes, eyebrows, mouth) TEXTURE_RESOLUTION_EXCEEDED
Bone influences per vertex ERROR 4 BONE_INFLUENCES_EXCEEDED
Leaf/end bones WARNING Must not have _end or _neutral suffixed bones LEAF_BONES_FOUND
Cameras in GLB ERROR Not allowed CAMERAS_FOUND
Lights in GLB ERROR Not allowed LIGHTS_FOUND
Animations in wearable GLB ERROR Not allowed (only emotes should have animations) ANIMATIONS_IN_WEARABLE
Forbidden material naming WARNING Must not contain _mouth, _eyebrows, _eyes outside facial categories FORBIDDEN_MATERIAL_NAME

Triangle limits per category (base, before Tris Combiner):

Category Limit
mask, eyewear, earring, tiara, top_head, facial_hair 500
hands_wear 1,000
hat, helmet, hair, upper_body, lower_body, feet 1,500
skin 5,000

Tris Combiner examples:

  • Jumpsuit (upper_body hiding lower_body): 1,500 + 1,500 = 3,000
  • Helmet hiding hair + hat: 1,500 + 1,500 + 1,500 = 4,500

Emote Validations

Check Severity Limit Code
Frame rate WARNING Expected 30 fps (tolerance: +/- 5 fps) EMOTE_FRAME_RATE
Max frames ERROR 300 EMOTE_MAX_FRAMES
Animation clip count ERROR 1 (basic), 2 (with props) EMOTE_MAX_CLIPS
Deform bone keyframes WARNING All deform bones must have position, quaternion, and scale tracks EMOTE_MISSING_KEYFRAMES
Avatar displacement ERROR 1 meter from starting position (euclidean distance on Hips bone) EMOTE_DISPLACEMENT
Armature naming WARNING Must be Armature, Armature_Prop, or Armature_Other ARMATURE_NAMING
Animation clip naming (with props) WARNING Clips must end with _Avatar or _Prop ANIMATION_NAMING
Audio format ERROR Only .mp3 and .ogg allowed AUDIO_FORMAT

Emote Prop Validations (when props are detected)

Check Severity Limit Code
Prop triangle count ERROR 3,000 PROP_TRIANGLE_COUNT
Prop material count ERROR 2 PROP_MATERIALS
Prop texture count ERROR 2 PROP_TEXTURES
Prop armature bones ERROR 62 PROP_ARMATURE_BONES

Screenshots

The screenshots are illustrative, as the styles of the errors were changed to be better under the dark theme

Screenshot 2026-03-18 at 1 36 07 PM Screenshot 2026-03-18 at 12 57 14 PM Screenshot 2026-03-18 at 12 57 08 PM

@vercel
Copy link

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
builder Ready Ready Preview, Comment Mar 19, 2026 11:44pm

Request Review

@coveralls
Copy link

coveralls commented Mar 18, 2026

Pull Request Test Coverage Report for Build 23322235472

Details

  • 350 of 393 (89.06%) changed or added relevant lines in 8 files are covered.
  • 2 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+1.5%) to 49.748%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/lib/glbValidation/wearableValidators.ts 119 121 98.35%
src/lib/glbValidation/emoteValidators.ts 172 175 98.29%
src/lib/getModelData.ts 2 14 14.29%
src/lib/glbValidation/index.ts 3 29 10.34%
Files with Coverage Reduction New Missed Lines %
src/lib/getModelData.ts 1 8.14%
src/modules/item/errors.tsx 1 47.62%
Totals Coverage Status
Change from base Build 23287840377: 1.5%
Covered Lines: 6656
Relevant Lines: 12091

💛 - Coveralls

Comment on lines +379 to +382
"title": "Your file has validation issues",
"has_errors": "Your file has validation errors that must be fixed before uploading.",
"triangle_count_exceeded": "Triangle count ({count}) exceeds the limit of {limit} for {category} wearables.",
"triangle_count_warning": "Triangle count ({count}) may exceed the limit for some wearable categories. The standard limit is 1,500 triangles.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

es and zh translations are en translations 😛

@@ -0,0 +1,39 @@
/** Severity level for a GLB validation issue. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the AI went extra verbose here. I think we can trim most of the comments since they are pretty self-explanatory already

@ShibuyaMartin
Copy link

Wearable Tris Combiner:

If the wearable hide other wearables the creator is allowed to combine the tris per slot. For example: if you want to do a jumpsuit you could create it using the upper body category hiding lower body; in that case you could have 1.5K2= 3K triangles.*

In the case of the helmet, if you hide all the head wearables (head, earrings, eyewear, tiara, hat, facial_hair, hair and top_head you can reach the 4k tris, 2 materials and 2 textures)

Does it have this into account @LautaroPetaccio ? docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants