Asset validation
Describes how validators check assets in the DCC before export, how project admins configure the rules, and what each built-in rule checks.
Validators catch problems before the file reaches Perforce or Unreal, such as a name that breaks the convention, a mesh that isn’t watertight, or a vertex skinned to too many bones. The Unreal plugin also checks asset names on import.
Enable and test a rule (project admin)
- Sign in to the web portal as an Admin. Click Projects, select your project, and click Validators.
- In the tree, select Global, an asset type such as Static Mesh, or a taxonomy node. Each rule is tagged inherited, overridden here or added here, and an inherited rule also shows the node it comes from.
- To add a rule, choose it from + add rule…. The validator reference shows which DCCs and asset types it covers. Use the toggle to turn the rule on or off, set the severity to error (blocks publishing) or warning (advisory), and enter its parameters. Editing an inherited rule creates an override at this node. To undo a change, click Reset on an overridden rule or Remove on a rule added here.
- The editor saves each change as you make it, so there is no Save button. To check a change, select another node and then select this one again.
- In the DCC, collect one valid and one invalid test instance for that node. The DCC caches conventions for up to five minutes, so wait or relaunch it, then click Validate.
- Check that the invalid instance shows the message you expect and, for an error, can’t be published. Check that the valid instance still passes.
If the project’s conventions are a Lua script, the tree is read-only. Change the rules in the script instead. See Authoring in Lua.
Resolve a validation failure (artist)
- Check only the instances you want to publish, and click Validate.
- Expand the failing rule to see the affected instance and its message. If the result lists objects, click Select Invalid to select them in the scene.
- If the rule has a Repair button, save your work and click it. Otherwise, fix the source by hand.
- After you change the scene’s structure, click Reset to collect again, then validate again. Before you publish, make sure no blocking error or destination conflict remains.
- If a project rule seems wrong, send the project admin the rule name, the message, the asset type and tags, and an example.
In Blender’s Dousen panel, the buttons are Run Validation, Fix and Publish Checked Instances. See the worked example.
Validator fields
Every validator, built-in or custom, is a Python class with these fields and a validate() method that
performs the check:
| Field | Meaning |
|---|---|
rule_id |
A stable, dotted identifier grouped by area, such as mesh.watertight,
naming.convention or texture.max_resolution. A convention uses it to turn the rule
on. |
name |
A readable name shown in the portal’s rule picker and in the DCC’s results list. |
category |
Groups rules in the picker, and decides whether the Nanite / NonNanite gate applies.
Examples are Mesh, Rig and Nanite. |
hosts |
The DCCs that load the validator, such as ["blender"]. ["any"] loads it in every
host. |
severity |
What a failure means by default. error blocks the export. warning is advisory, and the artist can continue. A convention can override this for a project. |
enabled |
Whether the rule is on by default. Some stricter or studio-specific rules ship turned off. |
asset_types |
The asset types the rule applies to: static_mesh, skeletal_mesh,
animation_sequence, texture, hda or vat. An empty list
means any type. |
params |
Typed values that admins can edit. Each is {key, type, default, label}, where type is int,
float, bool, string or list. Examples are max_tris = 50000 and target_fps = 30. |
The check returns whether it passed, a message such as "Exceeds limit of 50,000 tris", the affected objects, and an optional fix function. See Writing a custom validator.
The defaults on this page are the baseline. A project’s conventions can turn on a rule that is off by default, change a warning to an error, or tighten a parameter.
Where rules come from and how they are scoped
Validators are stored in the project’s naming-conventions document. You can attach a rule at three levels, and each applies to everything below it. For the entry format, see Validator entries.
- Global applies to every asset in the project.
- Asset type applies to every asset of one type.
- Taxonomy node applies to one node in the taxonomy tree, at any depth, and everything beneath it.
A deeper level overrides matching parameters from a shallower one, so a node can set a stricter texture limit for hero props than the global rule does. DousenCore stores each node’s effective rules in the compiled conventions, and a DCC uses the rules of the deepest matching node.
Texture size limits are validator rules (texture.max_resolution and
texture.resolution_power_of_2), so you set them on the Validators tab.
The validator catalog
The catalog is the list of rules that the portal’s rule picker offers. It has two layers:
- A compiled baseline built into DousenCore, with naming, mesh, UV, scene, material, rig, animation, texture, HDA and VAT rules. It’s the fallback when a tenant hasn’t reported anything.
- A layer reported by the DCC addons. On startup, each addon (Blender, Maya, 3ds Max, Houdini,
Substance Painter and Substance Designer) sends DousenCore the list of validator classes it has registered,
including custom plugins from
validators.extra_paths. Core accepts the report only from an Admin session, so an addon started by any other user skips it.
DousenCore merges the two layers for each tenant. A rule that appears only in the report is added to the catalog, with typed parameter fields in the rule picker. For a rule in both layers, Core keeps the baseline’s name, category, default severity and default state, and uses the report only to fill in parameters and asset types that the baseline leaves empty. When a DCC can’t reach the compiled conventions, it falls back to the validator catalog if DousenCore still serves it, and otherwise to the default rules built into the addon.
| Endpoint | Purpose |
|---|---|
GET /api/validator-catalog |
Returns the merged catalog (baseline plus reported rules) with each rule’s parameters. The portal’s rule picker uses it. Any signed-in user can call it. |
POST /api/validator-catalog |
A DCC addon reports its rule list for the tenant. The addon must be running in an Admin session. Other users get a 403. |
Validator reference
The built-in rules, by category. Default shows the default severity and whether the rule is on, and Parameters shows the baseline defaults. The Nanite, NonNanite and host-specific rules take their defaults from the addons, and the rest from DousenCore’s baseline catalog.
Naming
Every DCC reports these rules, which check names against the project’s compiled
conventions. In Substance Painter, Substance Designer and Houdini, they check texture
set, graph and HDA names. In those three, naming.convention checks only the prefix (Designer uses the
material prefix, default M_, on graph names), and naming.no_special_chars
doesn’t allow -. Houdini also allows . and :.
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
naming.convention |
error · on | any | — | Names start with the asset type’s prefix from the project convention (for example SM_ for a static
mesh), and the export name meets the convention’s pattern and length rules. Fails if the scene is unsaved, or if
the file name still contains an unresolved {placeholder}. |
naming.no_special_chars |
error · on | any | — | The file name and object names contain only letters, digits, _ and -. Maya also allows
| and :. |
Mesh: general hygiene
These geometry checks apply to every pipeline. The triangle limit is a general upper bound. The Nanite + Lumen —
Static preset raises it to 2,000,000, and Nanite + Lumen — Foliage turns it off. Both rely on
nonnanite.max_triangles for meshes without Nanite.
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
mesh.poly_count |
error · on | static_mesh, skeletal_mesh | max_tris = 50000 |
Each mesh has at most max_tris triangles. |
mesh.no_ngons |
warning · off | static_mesh, skeletal_mesh | — | No faces with five or more sides. |
mesh.no_zero_area |
error · on | static_mesh, skeletal_mesh | — | No faces with zero area. |
mesh.no_nonmanifold |
error · on | static_mesh, skeletal_mesh | — | No non-manifold geometry, such as an edge shared by more than two faces. In Blender, open boundary edges also count, so an open mesh fails this rule. |
Nanite: dynamic pipeline
For meshes that use Nanite and Lumen. Nanite needs no authored LODs, but open edges, flipped normals, floating
geometry and sliver triangles cause visible problems. These rules run only when the asset’s materials allow Nanite
(see the Nanite gate). uvs.min_coverage belongs to this category even though its ID
starts with uvs..
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
mesh.watertight |
error · on | static_mesh | — | Every edge borders exactly two faces, so there are no open boundary edges. Nanite renders holes as missing triangles, and Lumen leaks light through gaps. |
mesh.consistent_normals |
error · on | static_mesh, skeletal_mesh | — | No flipped or inconsistently wound faces, detected from edge winding. Flipped faces show up as pink in the Nanite viewport. |
mesh.no_sliver_triangles |
warning · on | static_mesh, skeletal_mesh | min_angle_deg = 5.0 |
No triangle has a corner angle smaller than min_angle_deg. |
mesh.no_loose_geometry |
error · on | static_mesh, skeletal_mesh | — | No loose vertices or wire edges that aren’t part of a face. Nanite can’t cluster them. |
nanite.min_triangles |
warning · on | static_mesh | min_tris = 1000 |
Each mesh has at least min_tris triangles, so Nanite’s fixed per-mesh cost is worth paying. Below
that, a traditional mesh is cheaper. |
nanite.tri_vert_ratio |
warning · on | static_mesh | max_vert_ratio = 0.6 |
The ratio of vertices to triangles stays at or below max_vert_ratio. Extra vertices point to UV or
normal seams that split geometry and hurt Nanite clustering. |
uvs.min_coverage |
warning · on | static_mesh | min_coverage = 0.65 |
UV0 islands fill at least min_coverage of the 0–1 space, which keeps texel density efficient. A
mesh with no UVs fails. |
NonNanite: traditional pipeline
These rules run instead of the Nanite rules when the asset’s materials don’t allow Nanite. They cover a traditional mesh with authored LODs and a baked lightmap.
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
nonnanite.max_triangles |
warning · on | static_mesh | max_tris = 60000 |
A triangle budget for meshes without Nanite, which cost more per triangle than Nanite meshes. Its default
(60,000) is tighter than mesh.poly_count only when a preset or node raises
mesh.poly_count, as Nanite + Lumen — Static does. |
nonnanite.lod_chain |
warning · on | static_mesh | min_lods = 1, tris_threshold = 20000 |
A mesh with more than tris_threshold triangles has at least min_lods authored LODs,
detected by name: <base>_LOD1, _LOD2 and so on. Meshes below the threshold need
no LODs. |
nonnanite.lightmap_uvs |
warning · on | static_mesh | — | The mesh has a second UV channel (UV1) for the baked lightmap. |
nonnanite.max_bounds_ratio |
warning · on | static_mesh | max_extent = 50 (m), dense_tris = 100000 |
Flags a mesh only when it is both large (its bounding-box diagonal is over max_extent, converted to
meters whatever the scene units) and dense (over dense_tris triangles). Only that combination culls
poorly without Nanite. |
UVs
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
uvs.in_range |
warning · off | static_mesh, skeletal_mesh | — | UV coordinates stay within the 0–1 tile. |
uvs.no_overlap_lightmap |
warning · on | static_mesh | — | No overlapping UVs in the lightmap channel. Overlaps bake as light bleed. |
Scene
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
scene.transforms_applied |
error · on | static_mesh, skeletal_mesh | — | Scale is 1 and rotation is 0 on every mesh object. Blender and Maya offer an automatic fix that applies rotation and scale. |
scene.no_hidden_objects |
warning · on | static_mesh, skeletal_mesh, animation_sequence | — | No mesh objects are hidden. Blender checks viewport and render visibility. Maya checks the visibility attribute. |
scene.single_root_skeletal |
error · off | skeletal_mesh | — | A skeleton has a single root bone (a single root joint in Maya). |
scene.no_loose_vertices |
error · on | static_mesh, skeletal_mesh | — | No vertices that aren’t connected to an edge. |
Materials
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
materials.no_missing_slots |
error · on | static_mesh, skeletal_mesh | — | No empty or unlinked material slots on a mesh. |
materials.expected_count |
warning · off | static_mesh, skeletal_mesh | max_materials = 3 |
No mesh has more than max_materials materials. See the note below this table. |
materials.no_empty_texture_sets |
warning · on | texture | — | Every texture set has at least one layer (Substance Painter). |
Max materials (max_materials) is an upper bound, and it defaults to 3 in Blender and Maya if
unset. A project that still sets the older expected_count parameter keeps the exact-match behavior: each
mesh must have exactly that many materials.
Rig
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
rig.bone_naming |
warning · on | skeletal_mesh | pattern = ^[a-z][a-z0-9_]*(\.[LR]|_[lr])?$ |
Bone names match the regular expression: lowercase, with an optional left or right suffix. |
rig.hierarchy |
error · on | skeletal_mesh | check_symmetry = false |
The skeleton has a single root bone. When check_symmetry is on, every left bone needs a matching
right bone. |
rig.bone_count |
warning · on | skeletal_mesh | max_bones = 256 |
The skeleton has at most max_bones bones. |
rig.max_influences |
error · on | skeletal_mesh | max_influences = 4 |
No vertex is skinned to more than max_influences bones. |
rig.unweighted_vertices |
error · on | skeletal_mesh | — | Every vertex has at least one skin weight. |
rig.root_at_origin |
error · on | skeletal_mesh | — | The root bone is at the world origin. |
rig.bind_pose |
warning · off | skeletal_mesh | — | The skeleton is in its bind pose, with no leftover rotations. In Blender, only armatures set to Rest Position are checked. |
rig.no_negative_scale |
error · on | skeletal_mesh | — | No bone has negative scale, which breaks in the engine. |
Animation
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
anim.frame_range |
error · on | animation_sequence | — | The clip has keyframes and a valid frame range. |
anim.fps_match |
error · on | animation_sequence | target_fps = 30 |
The scene frame rate equals target_fps. |
anim.root_motion |
warning · off | animation_sequence | expect_stationary = false, root_bone = root |
The root bone moves only when it should. Flags a moving root when expect_stationary is on, and a
stationary root when it is off. |
anim.no_keys_on_frame_zero |
warning · on | animation_sequence | — | No keyframes on frame 0. Unreal expects frame 1 or later. |
anim.no_scale_keys |
warning · on | animation_sequence | allow_root_scale = false, root_bone = root |
No scale keyframes on bones. When allow_root_scale is on, the root bone may have them. |
anim.bone_count_match |
error · off | animation_sequence | expected_bone_count = 0 |
The skeleton has exactly expected_bone_count bones. A value of 0 means any count, so the rule
passes. Set the real count to check it. |
Textures
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
texture.resolution_power_of_2 |
error · on | texture | — | The width and height of each texture set are powers of two. |
texture.max_resolution |
warning · on | texture | max_resolution = 4096 |
The width and height of each texture set are at most max_resolution. |
texture.channel_completeness |
error · on | texture | required_channels = [BaseColor, Normal, Roughness] |
Each texture set has every channel in required_channels. |
texture.channel_gamma |
error · on | texture | — | Color channels use sRGB, and data channels such as Normal and Roughness use Linear. The check also fails if the Painter version can’t report channel color spaces. |
Houdini: HDA and VAT
| Rule | Default | Applies to | Parameters | Checks |
|---|---|---|---|---|
hda.has_definition |
error · on | hda | — | Each selected node is an HDA instance with a definition. Fails if no node is selected. |
hda.version_set |
warning · on | hda | — | The HDA has a version string. |
hda.has_description |
warning · on | hda | — | The HDA has a label that differs from its type name. |
vat.resolution_power_of_2 |
error · on | vat | — | The VAT texture resolution parameters (posmap_res, normalmap_res and
vm_picture_res) are powers of two. |
vat.output_files_exist |
error · on | vat | expected_outputs = [posmap, normalmap, geo_output] |
Each parameter in expected_outputs exists on the node and has a path set. The check doesn’t test
whether the file is on disk. |
Host-specific rules
Some addons have checks of their own that aren’t in the baseline. They report themselves to the catalog on startup
like a custom plugin, together with the DCCs that implement them. The rule picker lists these rules for every node and
adds the hosts to the name, for example (blender only). The rule’s detail shows
hosts: followed by the DCCs. Only a host that implements the rule runs it, and a DCC that doesn’t
implement it skips the rule without a warning. If no Desktop has reported hosts yet, the catalog has none to show,
and a DCC without a validator for the rule shows the warning described in DCC coverage.
Only Designer’s two resolution rules (graph.resolution_power_of_2 and its
texture.resolution_power_of_2 alias) declare parameters, so the Textures rule can show Max
resolution (px) and Require square output fields that only Designer uses. The other
host-specific rules have no input fields in the portal.
3ds Max
| Rule | Default | Parameters | Checks |
|---|---|---|---|
scene.units |
error · on | unit_type = 5 |
The scene units match unit_type. The default, 5, is centimeters. |
scene.frame_rate |
error · on | fps = 24 |
The scene frame rate equals fps. |
scene.no_unused_layers |
error · on | — | No empty layers other than layer 0. |
rig.frozen_transforms |
error · on | tolerance = 0.0001 |
Every mesh has its position at zero and its scale at 1, within tolerance. If it fails, reset the
transform before you export. |
rig.no_bone_scale |
error · on | tolerance = 0.0001 |
No bone or Biped object has non-uniform scale. |
animation.range |
error · on | start_frame, end_frame |
The animation range starts and ends on the frames you set. The check passes if you set neither. |
animation.no_redundant_keys |
error · on | tolerance = 0.00001 |
No key repeats the value of the keys on either side of it. |
uvs.has_uvs |
error · on | — | Every mesh has UV channel 1. |
uvs.no_overlapping |
error · on | channel = 2 |
No two faces on channel, the lightmap channel by default, share the same UV vertices. This is a
duplicate-face check, not a full overlap test. |
materials.naming |
error · on | prefix = M_ |
Every material name starts with prefix. |
materials.no_missing |
error · on | — | No mesh is missing a material or uses a default material. |
Substance Designer
| Rule | Default | Parameters | Checks |
|---|---|---|---|
graph.output_completeness |
error · on | required_outputs = [BaseColor, Normal, Roughness] |
Every graph has an output node for each name in required_outputs. |
graph.resolution_power_of_2 |
error · on | max_resolution = 4096, require_square = true |
Each graph’s output size is square (when require_square is on) and no larger than
max_resolution. It doesn’t test for a power of two, because Designer stores sizes as power-of-two
exponents. Designer reports the same check as texture.resolution_power_of_2 too. |
graph.no_unconnected_nodes |
warning · on | — | No node is disconnected from the graph, with no input or output connections. |
DCC coverage
Blender, Maya and 3ds Max cover mesh authoring, and the other DCCs cover their own asset types. Every DCC reports the Naming rules.
| Category | Blender | Maya | 3ds Max | Painter | Designer | Houdini |
|---|---|---|---|---|---|---|
| Naming | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Mesh hygiene | ✓ | ✓ | ✓ | — | — | — |
| Nanite | ✓ | ✓ | ✓ | — | — | — |
| NonNanite | ✓ | ✓ | ✓ | — | — | — |
| UVs | ✓ | ✓ | ✓* | — | — | — |
| Scene | ✓ | ✓ | ✓* | — | — | — |
| Materials | ✓ | ✓ | ✓* | ✓ | — | — |
| Rig | ✓ | ✓ | ✓* | — | — | — |
| Animation | ✓ | ✓ | ✓* | — | — | — |
| Textures | — | — | — | ✓ | — | — |
| Graph | — | — | — | — | ✓ | — |
| HDA / VAT | — | — | — | — | — | ✓ |
3ds Max uses the same rule IDs as Blender and Maya for Naming, Mesh hygiene, Nanite and NonNanite, and also for
scene.transforms_applied, uvs.min_coverage, uvs.no_overlap_lightmap and
materials.no_missing_slots. In the categories marked ✓*, it covers the rest with its own
host-specific rules. For example, it checks the frame rate with scene.frame_rate
instead of anim.fps_match, and it has no rig.bone_count rule.
If a project turns on a rule that no DCC has a validator for, or before Desktop has reported which DCCs implement it, a DCC without a validator shows this warning: "No validator plugin found for rule 'rule id' on host 'host' — rule was skipped, not run."
The Nanite / NonNanite gate
Only one of the Nanite and NonNanite categories runs for each asset. Dousen picks it from the mesh’s material assignments in the project’s material catalog:
- If every assigned material resolves to a catalog entry and allows Nanite, the Nanite rules run.
- If every assigned material resolves and any of them doesn’t allow Nanite, the NonNanite rules run.
- If the mesh has no assignment, or any assignment doesn’t resolve to a catalog entry, the Nanite rules run, so existing scenes keep running the checks they always ran.
When Unreal imports a static mesh with a material assignment, it sets the mesh’s
NaniteSettings.bEnabled from the same material lookup, so in the common case the export checks match the
mesh’s Nanite setting. An unresolved assignment, or a geometry profile with nanite: false, can make
them differ. For the import rules, see How the import sets Nanite.
The Nanite / NonNanite gate is a preview feature. Which rules belong to each category, and how the gate handles edge cases such as unassigned or mixed materials, may change.
Writing a custom validator
To add your own checks, write a Python validator plugin and add its folder to the validators.extra_paths
pipeline setting. Each entry can be:
- a Perforce depot path, such as
//depot/tools/validators, - a path relative to the source root (
DOUSEN_SOURCE_ROOT), or - an absolute path.
For local testing, you can also set DOUSEN_VALIDATOR_PATHS to absolute folders, separated by your
platform’s path separator.
A validator subclasses ValidatorPlugin, sets the same fields as the built-in
rules, and implements validate(), so it gets the same kind of catalog entry, with typed parameter inputs
in the rule picker. Register it with the @register_plugin decorator. Import everything from
dousen.validators:
| Import | Role |
|---|---|
ValidatorPlugin |
The base class you subclass. |
register_plugin |
A decorator that adds your class to the registry, so Dousen discovers it and reports it to the catalog. |
CheckResult |
The return type, an alias for (bool, str, list[str], Callable | None). You can use it as the
return annotation. |
validate(self, context, params) must return a 4-tuple:
- passed (
bool):Trueif the check passed.validate()only reports pass or fail. The rule’sseveritydecides whether a failure blocks export (error) or is advisory (warning). - message (
str): the explanation shown next to a failed result. Use""when the check passes. - affected_objects (
list[str]): the names of the objects or faces that failed, so the panel can point at them. Use[]when the check passes. - fix_fn (
Callable | None): an optional function, with no arguments, that fixes the problem. Return it to make the panel show a Repair button (Fix in Blender) that runs it and then validates again. ReturnNonewhen there’s no safe automatic fix.
Import DCC modules inside validate() and the fix function. Dousen imports your class in every host, but
a host’s API (bpy, bmesh, pymxs, Maya cmds) exists only in the
hosts you list in hosts. The first argument is the host’s context: bpy.context in Blender,
and a dictionary in the other hosts. params holds the admin-configured parameters for this asset, so read
your keys from it with defaults.
PYTHON — a Blender custom validator, with an auto-fixfrom dousen.validators import ValidatorPlugin, CheckResult, register_plugin
@register_plugin
class NoNgonsValidator(ValidatorPlugin):
rule_id = "mesh.no_ngons_custom" # unique, dotted, namespaced
name = "No ngon faces" # shown in the picker + results list
category = "Mesh" # groups it in the picker
hosts = ["blender"] # which DCCs load it; ["any"] = all
severity = "warning" # default fail level; a convention may override
enabled = True # on out of the box
asset_types = ["static_mesh"] # [] = any type
params = [
{"key": "max_sides", "type": "int", "default": 4, "label": "Max sides"},
]
def validate(self, bpy_context, params) -> CheckResult:
import bmesh # DCC import stays inside validate()
limit = int(params.get("max_sides", 4))
offenders = []
for obj in bpy_context.scene.objects:
if obj.type != "MESH":
continue
bm = bmesh.new()
bm.from_mesh(obj.data)
ngons = sum(1 for f in bm.faces if len(f.verts) > limit)
bm.free()
if ngons:
offenders.append(f"{obj.name} ({ngons} ngons)")
if offenders:
# An auto-fix: triangulate the offending meshes on demand.
def _fix():
import bmesh
for obj in bpy_context.scene.objects:
if obj.type != "MESH":
continue
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(obj.data)
bm.free()
return False, f"{len(offenders)} mesh(es) contain ngons", offenders, _fix
return True, "", [], None # passed: empty message, no offenders, no fix
A new rule appears in the portal’s rule picker after an Admin’s DCC has reported it. To give every artist the same checks with no per-machine setup, keep custom validators in Perforce. See Loading studio tools.
Sharing one check across several DCCs
Many rules use the same logic in every DCC and differ only in how they read the scene. The built-in validators split such rules into two classes, and yours can too:
- A DCC-independent base class holds the metadata and implements
validate()once: the comparison, the list of offenders, and the 4-tuple. It leaves one data method abstract. - A subclass for each DCC sets
hostsand overrides only the data method, which returns raw measurements.
This is why mesh.poly_count appears for Blender, Maya and 3ds Max in the
coverage table.
PYTHON — the base owns validate(); it calls an abstract hookclass BasePolyCountValidator(ValidatorPlugin):
rule_id = "mesh.poly_count" # metadata lives on the base
name = "Polygon count within project limit"
category = "Mesh"
severity = "error"
asset_types = ["static_mesh", "skeletal_mesh"]
def get_mesh_tri_counts(self, context, params):
"""DCC-specific hook — subclass returns [(name, tri_count), ...]."""
raise NotImplementedError
def validate(self, context, params) -> CheckResult:
max_tris = int(params.get("max_tris", 50000))
offenders = []
for name, tris in self.get_mesh_tri_counts(context, params): # ← calls the hook
if tris > max_tris:
offenders.append(f"{name} ({tris:,} tris)")
if offenders:
return False, f"Exceeds limit of {max_tris:,} tris", offenders, None
return True, "", [], None # ← the 4-tuple is assembled here, once
PYTHON — the Maya subclass overrides only the data hook@register_plugin
class MayaPolyCountValidator(BasePolyCountValidator):
hosts = ["maya"] # the only metadata it adds
def get_mesh_tri_counts(self, context, params):
import maya.cmds as cmds # host import stays inside the hook
return [
(t, cmds.polyEvaluate(t, triangle=True))
for t in _mesh_transforms()
] # returns raw data — NOT the 4-tuple
The Maya subclass doesn’t override validate(), so the runner calls
BasePolyCountValidator.validate(). That method gets the raw counts from Maya’s
get_mesh_tri_counts(), applies the max_tris comparison, and returns the tuple.
Each base class defines the data method that its check needs:
| Base class | Abstract data method | Subclass returns |
|---|---|---|
BasePolyCountValidator | get_mesh_tri_counts | [(name, tris)] |
BaseWatertightValidator |
get_mesh_boundary_edge_counts | [(name, open_edge_count)] |
BaseConsistentNormalsValidator |
get_mesh_face_loops | [(name, [face_vert_indices])] |
BaseNoSliverTrianglesValidator |
get_mesh_triangles | [(name, [(p0, p1, p2)])] |
BaseNonNaniteMaxBoundsRatioValidator |
get_mesh_bounds_and_tris (and scene_unit_scale) | [(name, bbox_min, bbox_max, tris)] |
For a check that runs in one host, subclass ValidatorPlugin directly and return the 4-tuple. If the
same decision must run in two or more DCCs, use a base class with an abstract data method and a subclass for each
host, so the hosts can’t drift apart.
Validation history and project health
The DCC addons record each validation run in DousenCore, under the email address of the signed-in user. Each
instance you validate in the Publish tab logs its own run. DousenCore also stores runs that a studio
tool posts to POST /api/projects/:id/validators/log. The portal shows the runs in three places:
- Validation history lists the runs for the project, with the user, status and time. Click All, Pass, Warn or Error to filter by status. The list is paginated.
- The per-asset history shows the 100 most recent rule results for one asset. To open it, click an asset’s name on the Assets tab.
- The Health tab summarizes pass, warning and error trends and the top validator failures. The
default period is seven days. The API accepts a different
period, such as24h.
| Endpoint | Purpose |
|---|---|
POST /api/projects/:id/validators/log |
Records the result of a validation run. The DCC addons call it, and DousenCore takes the user from the authentication token. |
GET /api/projects/:id/health-summary |
Returns pass, warning and error trends, changelist activity and top offenders for a period. The free plan gets a 403. |
Admins and artists
- Admins choose which rules run, set severities and parameters, and add custom validators. Editing
rules in the Validators tab, or through the separate override document at
PUT /api/projects/:id/validators(which wins over the tree for every node that has the rule), requires the Admin role, and other users get a 403. The whole portal console, including validation history and the health dashboard, is Admin-only. See Roles on a studio instance. - Artists run the checks in the DCC at export time, fix errors, and review warnings. They can’t change which rules apply or their parameters.
Worked example: an asset fails a validator
This example uses Blender’s Dousen panel. Maya, 3ds Max, Substance Painter and Substance Designer work the same way with the Validate and Publish buttons on the Qt Publish tab. Houdini uses its Dousen: Validate shelf tool.
-
Run the validators.
In the Dousen panel, check the instances you want to publish, open the Validation panel, and click Run Validation. The panel lists the rules by category, and you can expand a failing rule to see its message and affected objects. Say
naming.conventionfails as an error because a name doesn’t start with the project’s prefix. The status bar shows "Dousen validation: 1 error(s), 2 warning(s)". -
Fix the error.
If the rule has a Fix button, click it. Dousen applies the fix and runs the validation again. Otherwise, rename the object yourself and click Run Validation again.
-
Try to publish with an error.
The Publish Checked Instances button in the Validation panel is disabled. If you click the button in the main Dousen panel instead, Blender shows "Fix validation errors before publishing", and nothing is exported or published.
-
Publish with only warnings.
Warnings don’t block publishing, but you have to confirm them. When only warnings remain (say, a
nonnanite.max_triangleswarning), Publish Checked Instances opens a dialog with the option Publish despite validation warnings. Turn it on to publish anyway, or cancel and fix the warnings first. If you publish without the option, Blender shows "Review validation warnings before publishing". On the Qt Publish tab, a dialog asks "N warning(s) found. Publish anyway?" instead.