<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged automate</title>
<link>https://coderlegion.com/tag/automate</link>
<description>Powered by Question2Answer</description>
<item>
<title>I Built a Batch Asset Validator for Blender in Python — Here's What I Learned</title>
<link>https://coderlegion.com/12945/i-built-a-batch-asset-validator-for-blender-in-python-heres-what-i-learned</link>
<description>&lt;h2 id=&quot;i-built-a-batch-asset-validator-for-blender-in-python-here-s-what-i-learned&quot;&gt;I Built a Batch Asset Validator for Blender in Python — Here's What I Learned&lt;/h2&gt;
&lt;p&gt;Game developers and 3D artists deal with a problem that doesn't get talked about enough: &lt;strong&gt;you find out your assets are broken after you've already submitted them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wrong scale. Tri count way over budget. Missing textures. Unapplied transforms. Negative scale that makes your mesh flip inside-out in Unity. These issues are obvious in hindsight, but catching them manually across a folder of 50 FBX files before every export is the kind of tedious work that either doesn't happen, or happens inconsistently.&lt;/p&gt;
&lt;p&gt;So I built AssetSentinel — a Blender addon that batch validates FBX and glTF assets against a target platform profile before they leave your machine. Here's the technical breakdown of how it works, what I ran into building it, and a full demo video at the end.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-problem-validation-belongs-before-export-not-after&quot;&gt;The Core Problem: Validation Belongs Before Export, Not After&lt;/h3&gt;
&lt;p&gt;Most 3D pipelines look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model → Export FBX/glTF → Import to Engine → Discover problem → Go back to Blender
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The discovery step is the expensive one. If you're submitting to a marketplace like Fab or CGTrader, a rejection costs you review time. If you're a studio delivering to a client, it costs credibility. The fix is moving validation to before the export step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model → Validate → Fix → Export FBX/glTF → Import to Engine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's what AssetSentinel does.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works-temp-scene-import-loop&quot;&gt;How It Works: Temp Scene Import Loop&lt;/h3&gt;
&lt;p&gt;The core mechanism is a &lt;strong&gt;modal operator&lt;/strong&gt; that imports each file into a temporary Blender scene, runs a series of checks, then destroys the scene and moves to the next file. This keeps the user's working scene completely untouched.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _evaluate_in_temp_scene(context, path: str, profile: dict) -&amp;gt; dict:
    win = context.window
    orig_scene = win.scene
    temp_scene = bpy.data.scenes.new(name=&quot;ASSETSENTINEL_TEMP&quot;)
    win.scene = temp_scene
    try:
        _import_file(path)
        # run all checks here
        tris, verts = _tri_vert_counts()
        ngons = _ngon_count()
        # ... more checks
    except Exception:
        # classify the import failure
        detail = _tb_last_meaningful_line(traceback.format_exc())
        ftype, suggestion = _classify_import_failure(detail)
    finally:
        win.scene = orig_scene
        bpy.data.scenes.remove(temp_scene)
        bpy.ops.outliner.orphans_purge(do_recursive=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block is non-negotiable. You have to restore the original scene and purge orphan data whether the import succeeded or failed — otherwise you accumulate stale mesh data across every file in the batch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-modal-operator-pattern&quot;&gt;The Modal Operator Pattern&lt;/h3&gt;
&lt;p&gt;Blender's UI locks up if you run long operations on the main thread. The solution is a &lt;strong&gt;modal operator&lt;/strong&gt; driven by a timer event. Each tick of the timer processes one file, then yields back to Blender so the UI stays responsive and the user can cancel with ESC.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def modal(self, context, event):
    wm = context.window_manager
    if event.type == &quot;ESC&quot;:
        wm.assetsentinel_cancel_requested = True
    if event.type != &quot;TIMER&quot;:
        return {&quot;PASS_THROUGH&quot;}
    if self._idx &amp;gt;= len(self._files):
        wm.assetsentinel_status = &quot;Done&quot;
        return self._finish(context, cancelled=False)
    # process one file per tick
    path = self._files[self._idx]
    res = _evaluate_in_temp_scene(context, path, prof)
    self._results.append(res)
    self._idx += 1
    wm.assetsentinel_progress = int((self._idx / len(self._files)) * 100)
    return {&quot;RUNNING_MODAL&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timer interval is 0.1 seconds. Fast enough to feel responsive, slow enough not to hammer the CPU.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;profile-based-validation&quot;&gt;Profile-Based Validation&lt;/h3&gt;
&lt;p&gt;Rather than hardcoding limits, AssetSentinel uses &lt;strong&gt;JSON profiles&lt;/strong&gt; — one per target platform. Each profile defines the limits that matter for that destination:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;UNITY_MOBILE&quot;: {
    &quot;label&quot;: &quot;Unity - Mobile&quot;,
    &quot;max_tris&quot;: 20000,
    &quot;max_texture_res&quot;: 2048,
    &quot;max_material_slots&quot;: 4,
    &quot;lod_required&quot;: true,
    &quot;no_spaces&quot;: true,
    &quot;min_bbox_m&quot;: 0.05,
    &quot;max_bbox_m&quot;: 10.0,
    &quot;tri_over_is_error&quot;: true,
    &quot;lod_missing_is_error&quot;: false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;_is_error&lt;/code&gt; flags per rule let you configure severity independently. A tri count violation might be a hard error on mobile, but only a warning on desktop. The profile system auto-merges on load — if a user has an older &lt;code&gt;profiles.json&lt;/code&gt;, new keys from the defaults get added without losing their custom values.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-checks&quot;&gt;The Checks&lt;/h3&gt;
&lt;p&gt;Here's what runs on every imported asset:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Geometry:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Triangle and vertex count via &lt;code&gt;me.calc_loop_triangles()&lt;/code&gt; — falls back to bmesh triangulate if that fails&lt;/li&gt;
&lt;li&gt;Ngon detection by checking &lt;code&gt;p.loop_total &amp;gt; 4&lt;/code&gt; on every polygon&lt;/li&gt;
&lt;li&gt;Negative scale on any axis&lt;/li&gt;
&lt;li&gt;Unapplied transforms (scale/rotation not at identity)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Textures:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Total image count from &lt;code&gt;bpy.data.images&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;External file resolution — whether the filepath exists on disk&lt;/li&gt;
&lt;li&gt;Max texture resolution from &lt;code&gt;img.size[0]&lt;/code&gt; and &lt;code&gt;img.size[1]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Missing external textures list&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Naming:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Regex match against &lt;code&gt;allowed_name_chars&lt;/code&gt; from the profile&lt;/li&gt;
&lt;li&gt;Space detection in object and material names&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Scale:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;World-space bounding box via &lt;code&gt;evaluated_get(depsgraph).bound_box&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Compared against &lt;code&gt;min_bbox_m&lt;/code&gt; and &lt;code&gt;max_bbox_m&lt;/code&gt; in the profile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LOD:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name-based detection — checks for &lt;code&gt;_LOD0&lt;/code&gt;, &lt;code&gt;_LOD1&lt;/code&gt;, &lt;code&gt;_LOD2&lt;/code&gt;, &lt;code&gt;_LOD3&lt;/code&gt; substrings&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;import-failure-classification&quot;&gt;Import Failure Classification&lt;/h3&gt;
&lt;p&gt;One of the more useful features: when an import fails, it doesn't just say &quot;import error.&quot; It classifies the failure and suggests a fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _classify_import_failure(detail_line: str) -&amp;gt; tuple[str, str]:
    s = (detail_line or &quot;&quot;).lower()
    if &quot;khr_texture_basisu&quot; in s:
        return (
            &quot;UNSUPPORTED_EXTENSION&quot;,
            &quot;Re-export without BasisU/KHR_texture_basisu and try again.&quot;
        )
    if &quot;bad gltf&quot; in s and &quot;json error&quot; in s:
        return (
            &quot;INVALID_GLTF&quot;,
            &quot;The glTF/GLB appears corrupted. Re-export from your DCC.&quot;
        )
    return (&quot;UNKNOWN&quot;, &quot;Try re-exporting and verify the file opens in Blender.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is genuinely useful in practice. &lt;code&gt;KHR_texture_basisu&lt;/code&gt; failures come up frequently with AI-generated GLB files and photogrammetry exports. The error message from Blender's importer isn't helpful on its own — this surfaces the actual fix.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-html-report&quot;&gt;The HTML Report&lt;/h3&gt;
&lt;p&gt;After the scan, results are exported as an HTML report with a CSV alongside it. The report uses a dark theme, shows per-asset pass/warning/fail status with a triangle count bar against the profile limit, and includes filter controls to show only failed or warning assets.&lt;/p&gt;
&lt;p&gt;The report also surfaces &quot;top violations&quot; — which checks failed most frequently across the batch. On a folder of 26 game assets, the most common failures were tri count overages and suspicious bounding box scales. Both are things that would have caused problems in the engine.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;watch-the-full-demo&quot;&gt;Watch the Full Demo&lt;/h3&gt;
&lt;p&gt;[embed: https://www.youtube.com/watch?v=64qQiMLCSD0]&lt;/p&gt;
&lt;p&gt;The video walks through a real batch scan of 26 assets against the Unreal Desktop profile — showing the scan running, reading the HTML report, and interpreting the results.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;get-the-addon&quot;&gt;Get the Addon&lt;/h3&gt;
&lt;p&gt;AssetSentinel is available on Superhive (Blender Market): &lt;a rel=&quot;nofollow&quot; href=&quot;https://superhivemarket.com/products/assetsentinel&quot;&gt;https://superhivemarket.com/products/assetsentinel&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Supports Blender 3.6 and above. No external dependencies — pure Python, no PyPI packages.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-d-do-differently&quot;&gt;What I'd Do Differently&lt;/h3&gt;
&lt;p&gt;A few things I'd approach differently if I started over:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results storage.&lt;/strong&gt; I'm serialising the results to JSON and storing them in a &lt;code&gt;WindowManager&lt;/code&gt; property string. It works but it's awkward — a proper addon-level data store would be cleaner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The modal pattern.&lt;/strong&gt; The timer-based modal is the right approach for Blender, but the 0.1s interval is a guess. A smarter approach would adapt the interval based on file size or import time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Profile UI.&lt;/strong&gt; Right now profiles are JSON files edited externally. An in-panel profile editor would make this accessible to artists who don't want to touch JSON.&lt;/p&gt;
&lt;p&gt;Happy to answer questions about any part of the implementation — drop them in the comments.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12945/i-built-a-batch-asset-validator-for-blender-in-python-heres-what-i-learned</guid>
<pubDate>Mon, 16 Mar 2026 13:58:07 +0000</pubDate>
</item>
</channel>
</rss>