<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Michael T. Wagner on Medium]]></title>
        <description><![CDATA[Stories by Michael T. Wagner on Medium]]></description>
        <link>https://medium.com/@mtw75?source=rss-b9afcb256636------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*TgWu7mTQ5gGw6-IqwE-xHg.png</url>
            <title>Stories by Michael T. Wagner on Medium</title>
            <link>https://medium.com/@mtw75?source=rss-b9afcb256636------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 10 Nov 2026 05:41:14 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mtw75/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Playing Video on USD Surfaces in NVIDIA Omniverse KIT and OVRTX]]></title>
            <link>https://mtw75.medium.com/playing-video-on-usd-surfaces-in-nvidia-omniverse-kit-and-ovrtx-b13a8dca0a32?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/b13a8dca0a32</guid>
            <category><![CDATA[synctwin]]></category>
            <category><![CDATA[nvidiaomniverse]]></category>
            <category><![CDATA[videotextures]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Sun, 18 Oct 2026 21:56:51 GMT</pubDate>
            <atom:updated>2026-10-18T22:15:44.620Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*dz4Syd-EggeoEwxOzsyseg.gif" /><figcaption>rendering a stage with video textures</figcaption></figure><h3>Video Textures in Kit 110.4 and a Standalone ovrtx Renderer</h3><p><em>An .mp4 is now just another texture. Here is how to use it in Omniverse Kit 110.4 and in a headless ovrtx render service, using one small demo scene.</em></p><p>A digital twin of a factory has screens on it: HMI panels, andon boards, a camera feed on the wall of the control room. Until now, putting moving video on those surfaces in Omniverse meant writing custom code. You swapped image files every frame, or built a dynamic texture provider by hand.</p><p>With Kit 110.4 and the RTX renderer behind it, that is no longer needed. You point a material’s texture input at an .mp4 file, the same way you would point it at a .png. The renderer decodes the video and shows the current frame on the surface.</p><p>This article walks through one demo scene, video_studio.usda, in two places:</p><ol><li><strong>Kit 110.4.</strong> Open the scene, press play, and watch the video on the surfaces.</li><li><strong>Standalone ovrtx.</strong> Render the same scene headless from Python, to a PNG, GIF or MP4, with no Kit app running. This is how we run it at SyncTwin, in a FastAPI render service.</li></ol><p>The second path has a few traps that cost us a day. On Windows the renderer can hang, and video can drift out of sync with the animation. Both are covered below, with the fixes.</p><h3>The big picture</h3><p>The scene file is the same in both renderers. Kit handles the video for you. A standalone ovrtx renderer needs two extra steps: the decoder plugins on Windows, and a renderer clock that you drive yourself.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*wwS7ZDcnhgiEVyEt69tXJg.png" /></figure><h3>Part 1: Video textures in Kit 110.4</h3><h3>The demo video</h3><p>The demo clip is textures/test_video.mp4: 10 seconds, 1280 × 720, 30 fps, about 4 MB. It shows a running timecode in one corner. That makes it easy to see which video frame is on screen at any moment, which helps a lot when you check sync later.</p><p>Any H.264 .mp4 works the same way. A short clip with visible timing is a better first test than a real camera feed.</p><h3>The material: an .mp4 where an image would go</h3><p>The scene uses a standard OmniPBR material. The only new thing is the file extension on the texture inputs:</p><pre>def Material &quot;VideoMaterial&quot;<br>{<br>    token outputs:mdl:surface.connect = &lt;/World/Looks/VideoMaterial/Shader.outputs:out&gt;     def Shader &quot;Shader&quot;<br>    {<br>        uniform token info:implementationSource = &quot;sourceAsset&quot;<br>        uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@<br>        uniform token info:mdl:sourceAsset:subIdentifier = &quot;OmniPBR&quot;<br>        # Kit 110.4: a video file, referenced like any image texture<br>        asset inputs:diffuse_texture = @./textures/test_video.mp4@ (<br>            colorSpace = &quot;auto&quot;<br>        )<br>        # Emissive, so the screen reads like a display even in a dark scene<br>        bool inputs:enable_emission = 1<br>        asset inputs:emissive_color_texture = @./textures/test_video.mp4@ (<br>            colorSpace = &quot;auto&quot;<br>        )<br>        float inputs:emissive_intensity = 1000<br>        token outputs:out<br>    }<br>}</pre><p>The video goes into both the diffuse and the emissive slot. Diffuse alone looks like a printed poster: it is only as bright as the lights hitting it. With emission on, the screen glows like a real display. It also lights up the dark studio around it a little.</p><h3>The geometry: meshes need UVs</h3><p>The scene has two surfaces with this material:</p><ul><li>/World/Screen: a flat, double-sided 16:9 mesh. Its UVs match the 1280 × 720 video, so nothing is stretched.</li><li>/World/Sphere: a UV sphere, to show the video wrapped around a curved surface.</li></ul><p>The sphere is a Mesh, not a Sphere prim. An implicit UsdGeom.Sphere has no primvars:st, so a texture has nothing to map onto. This is true of any texture, but it is easy to forget when you want a quick test object.</p><p>The rest of the scene is a photo studio: a dark, slightly glossy cyclorama, a warm key light, a cool rim light, and a camera at /World/Studio/Camera that frames both surfaces.</p><h3>Time: match the timeline to the clip</h3><p>The stage metadata sets the timeline to the length of the video:</p><pre>(<br>    startTimeCode = 0<br>    endTimeCode = 240<br>    timeCodesPerSecond = 24<br>    framesPerSecond = 30<br>)</pre><p>240 time codes at 24 per second is 10 seconds, which is the length of the clip. So when the timeline loops, the video loops with it. A render of the whole range also covers the whole clip.</p><h3>Open it in Kit</h3><p>Open video_studio.usda in a Kit 110.4 app with the RTX viewport, such as USD Composer or your own app built on the 110.4 templates. Switch to /World/Studio/Camera and press <strong>Play</strong>. The screen and the sphere show the video, and the timecode in the corner counts up.</p><p>You can also author it from the UI. Select the OmniPBR shader of any material, and in the Property panel set <strong>Albedo Map</strong> (and <strong>Emissive Color Map</strong>, if you want a glowing display) to the .mp4 file.</p><p>Note: video textures need Kit 110.4. If you build your own app from the Kit templates, check that its kit-kernel dependency is on 110.4 or later.</p><h3>Part 2: The same scene in a standalone ovrtx renderer</h3><p><a href="https://pypi.nvidia.com">ovrtx</a> is NVIDIA’s RTX renderer as a Python package, without Kit. You give it a USD stage and a render product, call step(), and read back the pixels. We use it in a small FastAPI service that renders previews and animations of SyncTwin scenes on a GPU server. The versions below are ovrtx==0.6.0 and ovstage==0.3.0.</p><p>The good news: the USD file does not change. The same video_studio.usda that plays in Kit renders in ovrtx, video and all.</p><h3>Step 1: Install ovrtx from NVIDIA’s index</h3><p>The ovrtx package on PyPI is only a stub. The real wheels come from NVIDIA&#39;s index:</p><pre>pip install --extra-index-url https://pypi.nvidia.com ovrtx==0.6.0.1791947691 ovstage==0.3.0.1791986420</pre><p>RTX needs Vulkan, not just CUDA. In Docker, give the container the graphics GPU capability as well as compute.</p><h3>Step 2 (Windows only): Give ovrtx its GStreamer plugins</h3><p>This is the step that cost us the most time. ovrtx decodes video through GStreamer, and it loads the plugins from one place only, inside its own package:</p><pre>&lt;site-packages&gt;/ovrtx/bin/plugins/gpu.foundation/gstreamer-1.0/</pre><p>The Linux wheels ship that folder. <strong>The Windows wheel does not.</strong> And without it, ovrtx does not report an error. The first step() on a scene with a video texture simply blocks forever, and takes the whole process with it.</p><p>Pointing GStreamer somewhere else does not help. GST_PLUGIN_PATH finds the plugins, but Windows then refuses to load their DLL dependencies, because ovrtx restricts the DLL search path. Adding the folder to PATH does not change that.</p><p>What works is copying the folder into the installed package. Kit already has a matching set: the omni.gpu_foundation extension carries it at bin/deps/gstreamer-1.0. In a Kit build you find it here:</p><pre>_build/windows-x86_64/release/extscache/omni.gpu_foundation-*/bin/deps/gstreamer-1.0</pre><p>We keep a copy in our repo (with Git LFS) and copy it into ovrtx when the service starts, before the renderer is created. Reinstalling ovrtx removes the folder, so doing it on every start is simpler than remembering:</p><pre>import shutil<br>from pathlib import Path<br>import ovrtx<br>def ensure_gstreamer_plugins(vendored: Path) -&gt; None:<br>    target = Path(ovrtx.__file__).parent / &quot;bin&quot; / &quot;plugins&quot; / &quot;gpu.foundation&quot; / &quot;gstreamer-1.0&quot;<br>    if target.is_dir():<br>        return<br>    # Copy beside the target, then rename, so a copy cut short<br>    # never looks like a complete install on the next start.<br>    partial = target.with_name(&quot;gstreamer-1.0.partial&quot;)<br>    shutil.rmtree(partial, ignore_errors=True)<br>    shutil.copytree(vendored, partial)<br>    partial.rename(target)<br><br>ensure_gstreamer_plugins(Path(&quot;third_party/gstreamer/windows-x86_64/gstreamer-1.0&quot;))<br>renderer = ovrtx.Renderer()  # only after the plugins are in place</pre><p>Two more Windows details:</p><ul><li><strong>Path length.</strong> Windows will not load a plugin whose full path is longer than 260 characters. GStreamer reports this only in its own debug log, so to ovrtx the plugin is just missing, and the render hangs again. Keep your virtualenv at a short path.</li><li><strong>Licences.</strong> The folder contains LGPL GStreamer plugins and some NVIDIA proprietary DLLs (DeepStream, CUDA NPP) that come with Kit. Check the Omniverse licence before you ship them outside your company.</li></ul><h3>Step 3: Render a still</h3><p>ovrtx renders a RenderProduct that is part of the stage. The pattern that works reliably is to compose a small root layer that sublayers your scene and adds the product, bound to the scene&#39;s camera:</p><pre>from pathlib import Path<br>import numpy as np<br>import ovrtx<br>import ovstage<br>from PIL import Image<br># A layer opened from a string has no folder of its own, so the sublayer<br># path must be absolute (forward slashes on Windows too).<br>scene = Path(&quot;video_studio.usda&quot;).resolve().as_posix()<br>LAYER = f&#39;&#39;&#39;#usda 1.0<br>(<br>    subLayers = [@{scene}@]<br>)<br><br>def Scope &quot;Render&quot;<br>{{<br>    def RenderProduct &quot;Product&quot;<br>    {{<br>        rel camera = &lt;/World/Studio/Camera&gt;<br>        int2 resolution = (1280, 720)<br>        rel orderedVars = &lt;LdrColor&gt;<br>        def RenderVar &quot;LdrColor&quot;<br>        {{<br>            string sourceName = &quot;LdrColor&quot;<br>        }}<br>    }}<br>}}<br>&#39;&#39;&#39;<br>renderer = ovrtx.Renderer()<br>stage = ovstage.Stage(name=&quot;demo&quot;)<br>renderer.attach_ovstage(stage)<br>ordinal = 1<br>ovstage.population.open_usd_from_string(stage, LAYER, ordinal=ordinal)<br>stage.advance_write_floor(ordinal).wait()  <br># seal the publication<br>products = {&quot;/Render/Product&quot;}<br>for _ in range(40):  # warmup: shaders, texture streaming, convergence<br>    renderer.step(render_products=products, delta_time=1 / 60, ordinal=ordinal)<br>result = renderer.step(render_products=products, delta_time=1 / 60, ordinal=ordinal)<br>frame = result[&quot;/Render/Product&quot;].frames[0]<br>var = frame.render_vars[&quot;/Render/Product/LdrColor&quot;].map(device=ovrtx.Device.CPU)<br>Image.fromarray(np.array(np.from_dlpack(var))).save(&quot;still.png&quot;)</pre><p>Some things to know:</p><ul><li><strong>Seal every publication.</strong> Each change to the stage goes in under a new ordinal, and you must call advance_write_floor(ordinal) before you step with it. In ovstage 0.3 that call is asynchronous, so .wait() on it. Otherwise step() fails with &quot;ordinal N exceeds ovstage write floor&quot;.</li><li><strong>Warm up.</strong> The first frames are noisy and textures are still streaming in. Around 40 steps gave us clean stills.</li><li><strong>Copy the pixels.</strong> The mapped buffer belongs to the renderer. Copy it with np.array(...) if you keep it past the next step, as you will in an animation.</li><li><strong>Render var keys.</strong> In ovrtx 0.5 and later, render_vars is keyed by the full prim path, such as /Render/Product/LdrColor. In 0.4 it was the bare name LdrColor.</li></ul><h3>Step 4: Render the animation to GIF or MP4</h3><p>A still shows one video frame. To see the video play, you render a series of frames and encode them. For each output frame you move the stage to that time, warm up a few steps, and capture:</p><pre>seconds = frame_index / fps<br>ordinal += 1<br>ovstage.population.update_from_usd_time(stage, ordinal, seconds)<br>stage.advance_write_floor(ordinal).wait()<br># ...then step and capture as before</pre><p>That is enough for time-sampled USD attributes, such as a cube moving along a path. <strong>For video it is not enough.</strong> Our first animated renders of the studio scene showed the video running ahead of the timeline. It was far ahead, and further with every frame.</p><h4>Video plays on the renderer’s clock, not on USD time</h4><p>The reason took a while to find. update_from_usd_time moves the USD stage. But a video texture does not read USD time. It plays on ovrtx&#39;s own simulation clock, and every step(delta_time=...) moves that clock forward.</p><p>So with 8 warmup steps per frame at delta_time=1/60, each output frame moves the video forward by 8/60 s, about 0.13 s. At 30 fps output, the frames are only 1/30 s (0.033 s) apart. The video runs four times too fast.</p><p>The fix is to drive the renderer clock yourself:</p><ol><li>Before the first frame, call renderer.reset(t0). This puts the clock at the first frame&#39;s time.</li><li>On the first warmup step of each later frame, pass delta_time equal to the gap to that frame&#39;s time.</li><li>On every other step, warmup and capture alike, pass delta_time=0. The clock stands still while the image converges.</li></ol><pre>def iter_frames(renderer, stage, times, products, warmup=40, frame_warmup=8):<br>    clock = None<br>    for index, seconds in enumerate(times):<br>        ordinal = stage.set_time(seconds)        # update_from_usd_time + seal<br>        if clock is None:<br>            clock = max(seconds, 0.0)            # the clock cannot go below zero<br>            renderer.reset(clock)<br>        advance = max(seconds - clock, 0.0)<br>        clock += advance<br>        steps = warmup if index == 0 else frame_warmup<br>        deltas = [advance] + [0.0] * steps       # move once, then hold still<br>        for delta in deltas:<br>            result = renderer.step(render_products=products, delta_time=delta, ordinal=ordinal)<br>        yield read_ldr_color(result)             # copy the pixels, as in step 3</pre><p>With this, the renderer clock and the USD time stay in step. The timecode in the demo video matches the timeline in every frame.</p><p>We test this with the demo clip. The test renders the studio scene at 1 s and 2 s, crops the screen from each frame, and compares it with every frame of the source .mp4. The closest match must be within 4 frames of 30 and 60. The decoder trails by a frame or so, so an exact match is too strict.</p><h4>Encode the frames</h4><p>For MP4 we pipe the RGB frames into imageio-ffmpeg (H.264, yuv420p, so it plays everywhere). For GIF we use Pillow. A GIF keeps every frame in memory until it is written, about one byte per pixel per frame, so keep GIFs short and small.</p><p>In our service, both come out of one command:</p><pre>uv run render-cli --usd tests/data/video_studio.usda \<br>    --camera /World/Studio/Camera --width 640 --height 360 --fps 30 -o studio.mp4</pre><p>With no --start or --end, it renders the scene&#39;s whole time range, from startTimeCode to endTimeCode. For the studio scene that is the full 10-second clip. In Git Bash, prefix the command with MSYS_NO_PATHCONV=1, so the /World/... camera path is not rewritten into a Windows path.</p><h3>Troubleshooting checklist</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/632/1*QKhLe4oGjr2Yf0YchKijZw.png" /></figure><h3>Wrap-up</h3><p>Video textures are a small change in USD, with a big effect on how real a digital twin feels. In Kit 110.4 it really is a single change: put an .mp4 where the image goes. In a standalone ovrtx renderer the USD stays the same. You add two things on your side: the GStreamer plugins on Windows, and a renderer clock that follows your animation.</p><p>The demo scene is a good test bed for both. Its timecode overlay shows sync problems at a glance, and its timeline matches the clip, so a render with no time range covers the whole video.</p><p>At SyncTwin we use this to show live-looking machine HMIs and camera feeds in rendered previews of factory layouts, from a render service with no Kit app running. If you try it, I’d like to hear what you put on your screens.</p><p>Here are source code and sample files for this article: <a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/video_textures">https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/video_textures</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b13a8dca0a32" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agentic 3D-Modelling: GPT6-Astra vs Opus 5.5]]></title>
            <link>https://mtw75.medium.com/agentic-3d-modelling-gpt6-astra-vs-opus-5-5-32cb1ebfa03d?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/32cb1ebfa03d</guid>
            <category><![CDATA[digital-twin]]></category>
            <category><![CDATA[blender3d]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[synctwin]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Sat, 03 Oct 2026 19:21:27 GMT</pubDate>
            <atom:updated>2026-10-03T19:21:27.376Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PImJc6fNXC8u0ogPaDQb6A.png" /><figcaption>GPT6:Astra vs Opus 5.5</figcaption></figure><p>Recent frontier models from OpenAI and Anthropic have made significant advances in 3D modeling. I put both to the test using the same prompts and reference data to see how their agentic 3D modeling capabilities compare.</p><h3>The model: ABENE VHF-680 CNC Milling Machine</h3><p>I’ve provided an image and a PDF document with the technical description of CNC milling machine ABENE VHF-680.</p><p>This machine presents a challenging test case due to its intricate geometry, including transparent components and protruding elements like the HMI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/546/1*4I3AyeySr0mfKob5SROXWw.png" /><figcaption>Original Image (PNG)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/732/1*McyS0vOUz9Cs7KAUXfb7gA.png" /><figcaption>Catalog Information (PDF)</figcaption></figure><h3>The Setup</h3><p>GPT-6 Astra was run through ChatGPT using the Computer Use plugin to control Blender 5.2, while Claude Opus 5.5 was run through Claude Code using the <a href="https://github.com/ahujasid/mcp-for-blender/tree/main">Blender MCP Addon</a> . Both also used direct python scripting.</p><h3>First shot</h3><p><strong>Initial Prompt:</strong></p><blockquote>Use Blender to rebuild this machine (image attached), here is more info on the machine : <a href="https://www.yumpu.com/de/document/view/1352354/abene-vhf-680-cnc">https://www.yumpu.com/de/document/view/1352354/abene-vhf-680-cnc</a> .</blockquote><p>Both models had an initial shot, it took a few minutes to build the model in Blender.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SYRPnavLH1W5MoyzT_rNYg.png" /><figcaption>Blender Remote Controlled</figcaption></figure><p>The initial result looked like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*40qx5T7pTB21GQw8lBzxfg.png" /><figcaption>GPT6-Astra First Shot</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HB2i-zeRceD4PjENLl1kCQ.jpeg" /><figcaption>Opus 5.5 First Shot</figcaption></figure><h3>Second Shot</h3><p>The quality didn’t match my expectations so I gave it another prompt, which both took about 15 minutes:</p><p><strong>Refinement prompt:</strong></p><blockquote>try again, try to be more accurate, also use pbr materials, we want to use it in a high end visualization</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CQM_XAExxMrwe3IFQA_BFQ.png" /><figcaption>GPT6-Astra Second Shot</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*w4ewbr6qzHrouaVovRvVCw.png" /><figcaption>Opus 5.5 Second Shot</figcaption></figure><p>Both results are quite nice, although its interesting how details have been modelled differently — it seems that the models have used different aspects of reference data to model the details — GPT seems to have used the images from the PDF document while Opus seems to have focused on the reference image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*veD3U2pB3x29LVxgeVumiQ.png" /><figcaption>Detail GPT6 Astra</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*H5KbHDKaDXrCNwDUNED_fw.png" /><figcaption>Detail Opus 5.5</figcaption></figure><h3>Conclusion</h3><p>Both models successfully reconstructed the machine in 3D using Blender and were able to further improve their results based on the second prompt.</p><p>Compared with <a href="https://mtw75.medium.com/generative-ai-image-to-3d-services-apis-a-benchmark-2fb119d96a95?sharedUserId=mtw75">diffusion-based GenAI approaches</a>, this agentic workflow has an important advantage: the resulting 3D models are structured in a meaningful way, with individual components such as the housing, control unit, and drive represented as separate, editable elements. This makes it possible to iteratively refine selected parts of the model rather than having to regenerate the entire asset.</p><p>At this point, however, we have a 3D model rather than a true digital twin. To get there, the models would also need to reconstruct the machine’s functional mechanisms and joints — for example, the movement of the milling axes and the opening and closing of the doors.</p><p>Given the current rate of improvement in frontier models, this next step may not be that far away. Exciting times!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=32cb1ebfa03d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Turn a Static JT File into a SimReady Robot with AI Agents]]></title>
            <link>https://mtw75.medium.com/how-to-turn-a-static-jt-file-into-a-simready-robot-with-ai-agents-6e79efbdc04b?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/6e79efbdc04b</guid>
            <category><![CDATA[digital-twin]]></category>
            <category><![CDATA[simready]]></category>
            <category><![CDATA[nvidia]]></category>
            <category><![CDATA[ai-agents-in-action]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 14:22:31 GMT</pubDate>
            <atom:updated>2026-09-23T14:22:31.769Z</atom:updated>
            <content:encoded><![CDATA[<h4>NVIDIA’s CAD-to-SimReady skills did the materials, physics and validation. A datasheet did the joints.</h4><p>We recently looked into agent-based workflows for digital twins in our OpenUSD study group, the <a href="https://perfectproducts.github.io/openusd_studierstube/">OpenUSD Studierstube</a>. <br>The challenge: take a vanilla JT file of a robot and see whether AI agents can turn it into a SimReady model, with all physics descriptions and joints.</p><p>Turns out it actually worked, and here is the step-by-step description. Note that the starting point was plain geometry: a flat list of part groups, not even a hierarchical model of the robot’s kinematic chain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TXwXoEzoiYSeubKoPem43w.png" /><figcaption>KUKA KR 270 R2700 ultra</figcaption></figure><p>This guide takes a JT file of a KUKA KR 270 R2700 ultra to a SimReady OpenUSD asset: PBR materials, colliders, mass and physics materials, six driven joints, validated and rendered. An AI coding agent (Claude Code) drives NVIDIA’s omniverse-cad-to-simready skill; you follow the steps below.</p><p><strong>SimReady</strong> means an OpenUSD asset that behaves correctly in simulation, not just looks right: correct units, physically based materials, collision shapes, mass, physics materials and, for robots, articulation. A profile of requirements defines it, and a validator checks it.</p><h3>Starting point: a flat JT file — kr270r2700ultra.jt</h3><p>This is the complete JT file after conversion to OpenUSD. It has nine sibling part groups with 42 meshes and cryptic part names. There is no parent–child chain, no rigid body, no joint, and no material beyond flat CAD colours.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GrdKAxUIfD_9nNjeswFdzA.png" /><figcaption>Flat JT File</figcaption></figure><p>Nothing here says which group is the base, which one moves, or about which axis. Working that out is most of the job.</p><h3>What you need</h3><ul><li>An NVIDIA GPU and Docker with GPU support (Docker Desktop with WSL2 on Windows; the containers reserve 8 CPUs).</li><li>Python 3.12 and uv.</li><li>A coding agent that can load skills (I used Claude Code), with the omniverse-cad-to-simready skill installed.</li><li>An API key for a vision-language model provider. OpenAI worked for me; see “What worked and what didn’t” for the others.</li><li>The CAD file, and the manufacturer’s datasheet for the robot.</li></ul><p>The pipeline you will run:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DKxEUewVM8vpO_HINkvcfA.png" /></figure><p>Each stage is a reference script in the skill. Every stage writes a JSON and a Markdown report, and hands its output_usd_path to the next stage. You can call the scripts one by one, or ask the agent to wrap them in a resumable runner as I did (cad2simready.py, which takes --from and --to for any stage).</p><h3>Step 1: Start the services</h3><ol><li>Put your provider key in ~/.omniverse-cad-to-simready/secrets.env, for example OPENAI_API_KEY=.... Never commit it.</li><li>Start the renderer. On Linux, the skill deploys OVRTX in Docker. On Windows, run a native OVRTX render service plus an adapter that serves the Content Agents rendering API on port 8001, as described in <a href="https://mtw75.medium.com/how-to-use-ovrtx-rendering-from-wsl2-containers-with-a-windows-wrapper-3e1479507607"><em>How to Use OVRTX Rendering from WSL2 Containers with a Windows Wrapper</em></a>.</li><li>Deploy the Material and Physics agents. On Windows, deploy them from WSL2, because preflight refuses a managed deployment on Windows.</li><li>Check the endpoints before you start the pipeline.</li></ol><pre>python ovrtx_adapter.py --backend http://127.0.0.1:8000 --port 8001        # Windows only<br>wsl -d Ubuntu -- bash -l ./deploy_content_agents_wsl.sh openai gpt-5.5    # material + physics agents<br>curl http://localhost:8001/health; curl http://localhost:8100/health; curl http://localhost:8200/health</pre><p>Ports used: OVRTX render service 8000, render API adapter 8001, Material Agent 8100, Physics Agent 8200.</p><p>After a reboot, start Docker Desktop and the adapter again. The containers come back on their own.</p><h3>Step 2: Give the pipeline context</h3><p>The material and physics agents only see renders of parts, so tell them what the object is. Run the context stage, then have the agent research the product and write pipeline/01_context/asset-context-research.json. The file holds the identity, cited sources, and material and physics hints, each marked as evidence or inference.</p><p>Its material_physics_prompt is passed to every agent downstream. Mine read, in short: <em>KUKA KR 270 R2700 ultra, about 1170 kg, cast metal links in glossy KUKA orange, dark grey motor and gearbox covers, chromed counterbalance rod, black rubber cable harness, steel or cast-aluminium density, metal friction about 0.5, low restitution.</em></p><p>You can also pass your own text with --prompt &quot;...&quot;.</p><h3>Step 3: Convert and assign materials and physics</h3><p>Run everything up to and including assignment:</p><pre>py -3.12 cad2simready.py testfiles\kr270r2700ultra.jt --to assign --assign-timeout 7200</pre><ul><li><strong>Convert</strong> uses NVIDIA’s <a href="https://github.com/NVIDIA-Omniverse/usd-convert-cad">usd-convert-cad</a> and keeps the CAD assembly tree. For the KR 270: 42 meshes in millimetres, Z up.</li><li><strong>Minimum gate</strong> checks that the stage opens and has a default prim, up-axis and units, before anything expensive runs.</li><li><strong>Material Agent</strong> renders each mesh, classifies it with a vision-language model and binds a SimReady library material.</li><li><strong>Physics Agent</strong> adds a collider, density and physics material to every mesh, plus a mass on the asset.</li></ul><p>Set --assign-timeout well above the 1800 s default; the material session alone took about 32 minutes here. If the client times out but the session finishes, reuse it with --resume-material-session &lt;id&gt; instead of paying for it again.</p><p>Check the result in pipeline/04_assign/: the material and physics OpenUSD file, an HTML report, and one prediction per mesh with its reasoning.</p><h3>Step 4: Add the joints</h3><p>JT carries no joints, so the robot is still one solid. Build the articulation from the datasheet:</p><ol><li><strong>Map links to axes.</strong> Inspect the CAD tree with bounds. For the KR 270, the groups k1…k7 are the chain from base to flange, and k9/k10 are the counterbalance.</li><li><strong>Take ranges and speeds from the datasheet.</strong> A1 ±185°, A2 -140/-5°, A3 -120/+155°, A4 ±350°, A5 ±122.5°, A6 ±350°, at 105 to 175 °/s (<a href="https://my.kuka.com/s/product/kr-270-r2700-ultra/01t58000002hnigAAA?language=de&amp;tab=Details">KUKA product page</a>).</li><li><strong>Find the pivots.</strong> Fit circles to the round axis caps, the wrist hub and the flange. Check the result against the datasheet reach (2694.5 mm fitted vs 2696 mm).</li><li><strong>Note the CAD pose.</strong> KUKA’s home pose here is A2 = -90°, A3 = +90°, so the joint limits in OpenUSD are shifted by that.</li><li><strong>Write it all to </strong><strong>robot-kinematics.json</strong> and author the rig as an OpenUSD layer over the physics file:</li></ol><pre>python author_robot_drives.py 04_assign\physics\kr270r2700ultra_material_physics.usd - `<br>    04_joints\kr270r2700ultra_articulated.usda `<br>    --kinematics 01_context\robot-kinematics.json --report 04_joints\drives.json</pre><p>The layer replaces the single root rigid body with one body per link. It adds six revolute joints with limits, angular drives with the datasheet speed limits, a fixed base, the counterbalance fixed to its links, and the articulation root.</p><h3>Step 5: Conform and validate</h3><p>Pick a SimReady profile, then let the pipeline author what it can and check the rest:</p><pre>py -3.12 cad2simready.py testfiles\kr270r2700ultra.jt --from conform --profile Prop-Robotics-Neutral</pre><ul><li>simready-conform-profile authors the profile&#39;s features. Here that was core metadata, units, and multi-body rigid bodies.</li><li>The validators then run: the Omniverse Asset Validator (asset, geometry, physics) and simready-validate for the profile.</li><li>If a failing requirement is auto-repairable, the pipeline conforms again and re-validates.</li><li>Read the result in pipeline/08_validate_rerun/*.md. Each failing requirement is listed by ID.</li></ul><p>If the profile includes grasp physics (GSP.001), render the asset, pick two grasp points, and rerun:</p><pre>py -3.12 cad2simready.py testfiles\kr270r2700ultra.jt --from conform --grasp-point x1,y1,z1 --grasp-point x2,y2,z2</pre><h3>Step 6: Render and check the report</h3><p>The render stage makes a 1024 × 1024 thumbnail with OVRTX, auto-framed from the asset bounds. It also inspects the pixels, so an all-black or uniform image fails the stage. The report stage collects everything into omniverse-cad-to-simready-report.md: each stage&#39;s status, the validation gates, the final OpenUSD file, and the reason for anything blocked.</p><p>Finally, open the articulated OpenUSD file in a UsdPhysics-aware simulator, press play, and drive the joint targets.</p><h3>The final structure</h3><p>The same nine groups are now rigid bodies with mass, connected by six revolute joints and three fixed joints under one articulation root. Every one of the 42 meshes also carries a SimReady material, a collider, a density and a physics material.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wYF6uZe5KrHpCpoXRBBjxA.png" /><figcaption>Final Structure with Joints</figcaption></figure><p>And this is the result in USD Composer. Joint A2 is selected: it rotates about Y, connects the rotating column k2 to the link arm k3, and is limited to -50° to 85°.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wgvLgghBTLEOG_Ibw3q1YA.png" /><figcaption>Final Result in NVIDIA Composer</figcaption></figure><p>The kinematic chain lives in the joints, not in the prim nesting: k1…k7 are still siblings, and the joints connect them base to flange. A2 and A3 limits are shifted from the datasheet because the CAD’s zero pose is KUKA’s home pose (A2 = -90°, A3 = +90°). Each revolute joint also has an angular drive and the datasheet speed limit.</p><p><strong>A word on the masses.</strong> The link masses are estimates, not manufacturer data. The Physics Agent put the whole robot at 746 kg; public sources give about 1170 kg. That total was split across the links by mesh volume × predicted density. This treats hollow castings as solid, can’t see motors and gearboxes the CAD doesn’t model, and inherits the agent’s density guesses (the chromed counterbalance rod came out as plastic). Centres of mass and inertias follow from the same assumptions. The result is good enough to make the asset move plausibly, but check it against the manufacturer’s datasheet or mass data if they are available before using it for dynamics, payload or controller tuning.</p><h3>What worked and what didn’t</h3><ul><li><strong>Platform.</strong> Worked: Windows 11 with Docker Desktop and WSL2, agents deployed from WSL. Didn’t: managed deployment from Windows (preflight refuses it), and OVRTX in Docker on WSL2 (no NVIDIA Vulkan/GL driver), so the renderer runs natively with an adapter.</li><li><strong>Context.</strong> Worked: agent web research with cited sources, and one prompt reused by every agent.</li><li><strong>Conversion.</strong> Worked: usd-convert-cad kept the assembly tree, units and scale. Didn’t: the empty ax* axis frames from JT carry no usable orientation.</li><li><strong>Materials and physics.</strong> Worked: the OpenAI backend turned 42 meshes into 5 SimReady materials and 14 physics materials, with colliders and densities. Didn’t: NVIDIA-hosted gemma-4–31b-it (529 overloaded, then “At most 4 image(s)”), an Anthropic account without credit, and the client timeout at the 1800 s default.</li><li><strong>Joints.</strong> Worked: datasheet values plus circle-fitted pivots gave 6 revolute joints with drives, and the validator was clean. Didn’t: the Joint Agent (research preview) authored 0 joints in 5 runs. It listed the root prim with a null segment, the CAD repeats child names (e1vc under 5 links), and there was a single root rigid body. A custom prompt-library entry fixed the structure step, but no candidate became ready.</li><li><strong>Conform.</strong> Worked: automatic repairs for UN.007, NP.006 and RB.MB.001. Didn’t: GSP.001 grasp points need a human, and 18 dangling over prims fail the asset gate.</li><li><strong>Render.</strong> Worked: OVRTX thumbnail with a pixel check.</li></ul><p>The short version: the skills automate the tedious parts well, and let the agent do conversion, per-mesh material and physics classification and profile repairs. Anything kinematic still needs real data about the product, and the datasheet was worth more than any model.</p><h3>Resources on SimReady</h3><p>Start with the overview and the specification, then use SimReady Foundation to look up what a failing requirement ID means.</p><ul><li><a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/simready_robot">Example: simready_robot</a>: this article’s files in the OpenUSD Studierstube repo (converted, material + physics and articulated OpenUSD files, kinematics spec, reports, pipeline scripts).</li><li><a href="https://docs.omniverse.nvidia.com/simready/latest/index.html">SimReady Overview</a>: NVIDIA’s entry point: what SimReady is, simulation needs, asset creation, glossary.</li><li><a href="https://docs.omniverse.nvidia.com/simready/latest/overview/simready-spec.html">The SimReady Specification</a>: how assets are made simulation-ready through metadata and core OpenUSD schemas.</li><li><a href="https://docs.omniverse.nvidia.com/simready/latest/simready-faq.html">SimReady FAQ</a>: common questions about the standard.</li><li><a href="https://docs.omniverse.nvidia.com/simready/latest/simready-asset-creation.html">SimReady 3D Art Asset Creation</a>: modeling, UV, material and physics best practices when building assets by hand.</li><li><a href="https://docs.omniverse.nvidia.com/simready/latest/simready-asset-creation/physics-best-practices.html">Physics Best Practices</a>: colliders, mass in kg, friction and restitution with UsdPhysics.</li><li><a href="https://github.com/NVIDIA/simready-foundation">SimReady Foundation</a>: requirements, capabilities, features and profiles (e.g. Prop-Robotics-Neutral, Robot-Body-Runnable), plus validators and conform skills.</li><li><a href="https://docs.omniverse.nvidia.com/kit/docs/asset-validator/latest/index.html">Omniverse Asset Validator</a>: rule-based OpenUSD validation with auto-fixes, as a Kit UI or a standalone CLI.</li><li><a href="https://github.com/nvidia/skills">NVIDIA Agent Skills</a>: official skill catalog for Claude Code, Codex and other agents, including omniverse-cad-to-simready.</li><li><a href="https://github.com/nvidia-omniverse/content-agents">USD Content Agents</a>: Material and Physics agents (beta); Texture, Joint, Geometry and Validation agents (research preview).</li><li><a href="https://github.com/NVIDIA-Omniverse/usd-convert-cad">usd-convert-cad</a>: the CAD-to-OpenUSD converter used by the convert stage.</li><li><a href="https://nvidianews.nvidia.com/news/nvidia-agent-toolkit-expands-with-new-omniverse-libraries-putting-ai-agents-to-work-building-simulation-ready-worlds">NVIDIA Agent Toolkit announcement</a>: the 20 July 2026 launch of the CAD-to-SimReady skills.</li></ul><p>Install the skill with npx skills add https://github.com/nvidia/skills --skill omniverse-cad-to-simready.</p><h3>Sources</h3><ul><li><a href="https://github.com/NVIDIA-Omniverse/usd-convert-cad">NVIDIA usd-convert-cad</a></li><li><a href="https://my.kuka.com/s/product/kr-270-r2700-ultra/01t58000002hnigAAA?language=de&amp;tab=Details">KUKA KR 270 R2700 ultra product page</a> (technical data, opened 21 Sep 2026)</li></ul><h3>Acknowledgements</h3><p>Thanks to <a href="https://www.linkedin.com/in/benjamin-v%C3%B6lzke-7132bb164/">Ben Völzke</a> for providing the KUKA JT file used in this article.</p><p><em>This article was written with the help of Claude Opus 5 (Anthropic) in Claude Code, which also drove the pipeline as the coding agent described above. The Content Agents used OpenAI models for their own predictions: gpt-5.5 for materials and physics, gpt-4.1 and gpt-4.1-mini in the Joint Agent.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e79efbdc04b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Use OVRTX Rendering from WSL2 Containers with a Windows Wrapper]]></title>
            <link>https://mtw75.medium.com/how-to-use-ovrtx-rendering-from-wsl2-containers-with-a-windows-wrapper-3e1479507607?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/3e1479507607</guid>
            <category><![CDATA[ovrtx]]></category>
            <category><![CDATA[omniverse]]></category>
            <category><![CDATA[nvidia]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Mon, 21 Sep 2026 14:42:13 GMT</pubDate>
            <atom:updated>2026-09-21T15:25:56.389Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sL3x5I8vPYNYPRntfgJamw.png" /></figure><p>NVIDIA’s OpenUSD Content Agents render every part of your asset before a vision-language model decides its material, physics or joints. They expect an OVRTX rendering service next to them in Docker. On Linux that works out of the box. On Windows with Docker Desktop and WSL2, the OVRTX container dies at startup.</p><p>While building a SimReady robot asset from CAD, we got around this by keeping the agents in Docker and running the renderer natively on Windows. A 340-line Python adapter makes the native renderer look exactly like the containerised one. This article shows how to set it up and how the adapter works.</p><h3>Why OVRTX fails in Docker on WSL2</h3><p>WSL2 doesn’t pass the GPU through the way native Linux does. The Windows NVIDIA driver exposes a paravirtualised GPU to WSL, with CUDA, DirectX 12 and OptiX libraries mounted from the host (/usr/lib/wsl/lib). There is no NVIDIA Vulkan or OpenGL driver in that set.</p><p>OVRTX is an RTX renderer and needs the NVIDIA graphics stack, not just CUDA. The upstream container ships an NVIDIA Vulkan ICD file and expects the driver behind it. On WSL2 there is none, so the renderer daemon exits during initialisation. CUDA-only containers, including the agents themselves, run fine.</p><p>The practical conclusion: keep everything that only needs CUDA or the network in Docker, and move the renderer to the Windows host, where the full NVIDIA driver is available.</p><h3>The architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E-djPDDKoAlOZfnxtZLfdg.png" /><figcaption>Three pieces, two of them on the Windows host</figcaption></figure><p>Three pieces, two of them on the Windows host:</p><ul><li><strong>Content Agents</strong> run in Docker on WSL2. They speak the Content Agents ovrtx-rendering-api: an OpenUSD scene as a data URI or URL, a list of cameras and frames, and base64 PNGs back.</li><li><strong>The adapter</strong> (ovrtx_adapter.py) runs on Windows, port 8001. It serves that API and translates each request to the job API below.</li><li><strong>The OVRTX render service</strong> runs on Windows, port 8000. It is a job queue around NVIDIA ovrtx: one OpenUSD file path and one camera per job, a PNG as the result.</li></ul><p>The containers reach the host through http://host.docker.internal:8001. The agents don&#39;t know anything changed: they see the same /health and /render endpoints the upstream OVRTX container would serve.</p><h3>Setting it up</h3><p><strong>1. Run an OVRTX render service on Windows.</strong> You need a native process that owns the GPU and renders an OpenUSD file from a camera. Ours is a small FastAPI service around NVIDIA’s ovrtx Python renderer on port 8000. My previous article, <a href="https://mtw75.medium.com/how-to-build-a-simple-ovrtx-render-service-on-windows-be4fec701da7"><em>How to Build a Simple OVRTX Render Service on Windows</em></a>, walks through building it; the code is in <a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/ovrtx_render_service">examples/ovrtx_render_service</a>. A single worker thread holds the one Renderer, because it is single-threaded and loading a scene is expensive. The API has four endpoints:</p><ul><li>POST /render: submit {usd_path, camera_path, width, height, force_reload, priority} and get a job_id back.</li><li>GET /jobs/{id}: job status (queued, running, done, failed or cancelled).</li><li>GET /jobs/{id}/result: the rendered PNG.</li><li>GET /health: {status, worker_alive, pending, running}.</li></ul><p><strong>2. Start the adapter.</strong> Get <a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/ovrtx_wsl_adapter">ovrtx_adapter.py</a> and run it with Python 3.10 or newer; it uses only the standard library. OpenUSD (pxr, for example pip install usd-core) is needed only for animated cameras.</p><pre>python ovrtx_adapter.py --backend http://127.0.0.1:8000 --port 8001</pre><p>It listens on 0.0.0.0:8001, so the WSL2 containers can reach it. Allow the port in Windows Firewall if you are prompted.</p><p><strong>3. Point the containers at the host.</strong> The agents read the render endpoint from RENDER_ENDPOINT. Set it before deploying and make sure host.docker.internal resolves:</p><pre>services:<br>  joint-agent-service:<br>    environment:<br>      - JA_RENDER_BACKEND=remote<br>      - RENDER_ENDPOINT=${RENDER_ENDPOINT:-http://host.docker.internal:8001}<br>    extra_hosts:<br>      - &quot;host.docker.internal:host-gateway&quot;</pre><p>For the Material and Physics agents, export RENDER_ENDPOINT=http://host.docker.internal:8001 in the WSL shell before running the skill&#39;s deploy step.</p><p><strong>4. Verify.</strong> From Windows, and from inside a container (if its image has curl; otherwise use python -c with urllib):</p><pre>curl http://127.0.0.1:8001/health<br>docker exec content-material-agent-service curl -s http://host.docker.internal:8001/health</pre><p>A healthy answer reports &quot;status&quot;: &quot;healthy&quot;, &quot;renderer&quot;: &quot;ovrtx&quot; and the backend&#39;s own health under backend_health. Then run any agent session and watch the adapter log rendered 1 frame(s) x 4 camera(s) in …s.</p><h3>Inside the adapter</h3><p>The adapter is a standard-library ThreadingHTTPServer with two endpoints. Its job is to close the gap between two contracts:</p><ul><li><strong>Scene.</strong> The agents send a url: a data URI, http(s) URL, file URL or path, holding a plain OpenUSD layer or a zip/USDZ bundle. The job API wants a usd_path on the local disk.</li><li><strong>Cameras.</strong> The agents send a list of camera prims; the job API renders one camera per job.</li><li><strong>Frames.</strong> The agents can ask for a frame_range; the job API only renders the stage&#39;s default time.</li><li><strong>Result.</strong> The agents expect synchronous JSON with images[frame][camera][&quot;images&quot;] as base64 PNGs; the job API returns a job id to poll, then the PNG as a download.</li></ul><p>A /render request goes through five steps:</p><ol><li><strong>Materialise the payload.</strong> Decode the data URI (or download or copy the file) and hash the bytes. Store it once per hash under the work directory; unzip bundles safely, rejecting entries that escape the folder, and pick the root layer.</li><li><strong>Bake animated cameras.</strong> If the request asks for frames other than 0, write one small layer that sublayers the scene and adds a static copy of each camera per frame, with its transform and lens attributes evaluated at that time.</li><li><strong>Render serially.</strong> Under one lock, submit one job per (frame, camera) and poll /jobs/{id} until it finishes. Download the PNG and check it really is one.</li><li><strong>Answer in-band.</strong> Return {status: &quot;success&quot;, images}, or {status: &quot;exception&quot;, error} with HTTP 200, as the upstream contract expects.</li><li><strong>Clean up</strong> the per-request work folder, unless --keep is set for debugging.</li></ol><p>Three design decisions matter for performance:</p><ul><li><strong>Content-addressed cache.</strong> The agents resend the same multi-megabyte scene for every view of every part. Storing it by hash gives the backend a stable file path. The backend only reloads the scene (force_reload) when that path changes, so dozens of views share one expensive scene load.</li><li><strong>One layer for all frames.</strong> Baking every frame into sibling cameras of a single layer, instead of a layer per frame, avoids a full scene reload per image.</li><li><strong>A deep listen queue.</strong> The Joint Agent fires dozens of concurrent /render calls. The stdlib default backlog of 5 refuses most of them, so the server raises request_queue_size to 512. Requests simply wait for the render lock.</li></ul><h3>Limitations and gotchas</h3><ul><li><strong>RGB only.</strong> The job API returns colour images. Requested sensors such as depth or segmentation come back as empty strings, with a warning in the response. The Material Agent only asks for RGB. Check your agent before relying on the others.</li><li><strong>One render at a time.</strong> The renderer is single-threaded, so the adapter serialises everything. A 42-part robot took the Material Agent about 32 minutes. Raise the agents’ client timeouts accordingly; the adapter’s own per-job timeout is 1800 s (--job-timeout).</li><li><strong>The cache only grows.</strong> Materialised scenes stay in %TEMP%\ovrtx_adapter\cache by content hash. Delete that folder now and then.</li><li><strong>Background masks are ignored.</strong> apply_background_mask has no equivalent in the job API.</li><li><strong>Nothing restarts by itself.</strong> After a reboot, Docker Desktop brings the agent containers back, but the render service and the adapter are plain Windows processes. Start both again, or register them as scheduled tasks. A stopped Docker daemon or adapter looks like an agent that never finishes, so check /health first.</li><li><strong>Ports.</strong> Port 8000 is taken by the render service, so move any container that defaults to 8000 (the Joint Agent here) to another host port, and let 8001 through the Windows firewall.</li></ul><h3>Wrapping up</h3><p>If a GPU service in Docker on WSL2 needs graphics rather than just CUDA, you don’t have to move the whole stack to Linux. Run the graphics part natively on Windows, and put a thin adapter in front that speaks the API the containers already expect. The agents stay unchanged, and the adapter’s caching and frame baking keep the native renderer efficient.</p><p>The same pattern works for any containerised client whose upstream renderer can’t start under WSL2. Match the contract, translate the payload, serialise the GPU.</p><h3>Resources</h3><p>The adapter, with a README covering options, API and limitations: <a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/ovrtx_wsl_adapter">examples/ovrtx_wsl_adapter</a>.</p><ul><li><a href="https://github.com/nvidia-omniverse/ovrtx">NVIDIA ovrtx</a> (<a href="https://nvidia-omniverse.github.io/ovrtx/">docs</a>): C and Python library for Omniverse RTX rendering and sensor simulation of OpenUSD scenes.</li><li><a href="https://github.com/nvidia-omniverse/content-agents">USD Content Agents</a>: the Material, Physics and Joint agents, and the upstream ovrtx-rendering-api the adapter mimics.</li><li><a href="https://docs.nvidia.com/cuda/wsl-user-guide/index.html">CUDA on WSL User Guide</a>: what the NVIDIA driver supports in WSL2 (CUDA, DirectX and DirectML); OpenGL-based applications won’t work.</li><li><a href="https://docs.docker.com/desktop/features/gpu/">Docker Desktop GPU support</a>: GPU paravirtualisation for containers on the WSL2 backend.</li></ul><p><em>This article and the adapter were written with the help of Claude Opus 5 (Anthropic) in Claude Code.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3e1479507607" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Build a Simple OVRTX Render Service on Windows]]></title>
            <link>https://mtw75.medium.com/how-to-build-a-simple-ovrtx-render-service-on-windows-be4fec701da7?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/be4fec701da7</guid>
            <category><![CDATA[synctwin]]></category>
            <category><![CDATA[omniverse]]></category>
            <category><![CDATA[nvidia]]></category>
            <category><![CDATA[ovrtx]]></category>
            <category><![CDATA[openusd]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Mon, 21 Sep 2026 13:10:18 GMT</pubDate>
            <atom:updated>2026-09-21T14:28:37.616Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qwgdyPvyxzBVoTe1iF9Ypw.png" /></figure><p>NVIDIA’s <a href="https://github.com/nvidia-omniverse/ovrtx">ovrtx</a> puts Omniverse RTX rendering into a Python package: open a USD stage, step the renderer, get a PNG. That is perfect for a script. But once several tools need renders, such as AI agents in Docker, a web front end or a batch pipeline, you want one process that owns the GPU and takes requests over HTTP.</p><p>This article builds that process on Windows in about 250 lines of Python: a FastAPI service with a job queue and a single render worker. Clients submit an OpenUSD file and a camera, poll the job, and download the PNG.</p><h3>What you need</h3><ul><li>Windows 10 or 11 with an NVIDIA RTX GPU and a current driver.</li><li>Python 3.10 to 3.13. I use uv to manage the environment.</li><li>The ovrtx and ovstage wheels from PyPI, plus fastapi, uvicorn, numpy and pillow.</li><li>An OpenUSD scene that contains a camera. The service renders from existing cameras; it doesn’t place its own.</li></ul><pre>[project]<br>name = &quot;ovrtx-render-service&quot;<br>requires-python = &quot;&gt;=3.10,&lt;3.14&quot;<br>dependencies = [<br>    &quot;ovrtx==0.5.0.377615&quot;,<br>    &quot;ovstage==0.2.0.377349&quot;,<br>    &quot;fastapi&gt;=0.115&quot;,<br>    &quot;uvicorn&gt;=0.30&quot;,<br>    &quot;numpy&gt;=2.0&quot;,<br>    &quot;pillow&gt;=10.0&quot;,<br>]</pre><p>Pin the ovrtx and ovstage versions: the API moved between releases. Version 0.4 switched to an “attached” stage, and 0.5 changed how render outputs are keyed. Run everything from PowerShell or cmd. In my setup, the same script failed from Git Bash with failed to parse inline USDA into anonymous layer and worked from PowerShell.</p><h3>The design</h3><p>Two facts about ovrtx shape the whole service. The Renderer is single-threaded, and loading a scene is expensive: shader compilation, texture streaming and warm-up frames. So exactly one thread may touch the GPU, and a loaded scene should stay loaded.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O7x99toKgznm-Z0dYY3Gmw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*LfR-0JZTgFCGDWJZMOzeAw.png" /></figure><p>The HTTP handlers only read and write the job store. The worker takes the highest-priority job, renders it and writes the PNG to disk. Clients poll until the job is finished; renders take from half a second to a minute, too long to hold a request open.</p><h3>Step by step</h3><h3>1. Render one image</h3><p>Start without a service. ovrtx renders <em>render products</em>: a RenderProduct prim binds a camera and a resolution to outputs such as LdrColor. Put the scene, a dome light and the product into one small root layer:</p><pre>def scene_layer(usd: str) -&gt; str:<br>    return f&#39;&#39;&#39;#usda 1.0<br>(<br>    subLayers = [@{asset_path(usd)}@]<br>)<br>def DomeLight &quot;RenderSvcSky&quot;<br>{{<br>    float inputs:intensity = 1000<br>}}<br>&#39;&#39;&#39;<br><br>def product_layer(camera: str, width: int, height: int) -&gt; str:<br>    return f&#39;&#39;&#39;#usda 1.0<br>def Scope &quot;RenderSvc&quot;<br>{{<br>    def RenderProduct &quot;Product&quot;<br>    {{<br>        rel camera = &lt;{camera}&gt;<br>        int2 resolution = ({width}, {height})<br>        rel orderedVars = &lt;LdrColor&gt;<br><br> def RenderVar &quot;LdrColor&quot;<br>        {{<br>            string sourceName = &quot;LdrColor&quot;<br>        }}<br>    }}<br>}}<br>&#39;&#39;&#39;</pre><p>Since ovrtx 0.4 the renderer attaches to an ovstage.Stage that you own. Every change to the stage is published under an <em>ordinal</em>, and every step names the ordinal it should see. Seal each ordinal with advance_write_floor, or the step fails:</p><pre>renderer = ovrtx.Renderer()            # first run compiles shaders<br>stage = ovstage.Stage(&quot;render-one&quot;)<br>renderer.attach_ovstage(stage)<br><br>ovstage.population.open_usd_from_string(stage, layer, ordinal=1)<br>stage.advance_write_floor(1)<br><br>for _ in range(40):                    # warm-up: converge and stream textures<br>    renderer.step(render_products={PRODUCT}, delta_time=1 / 60, ordinal=1)<br>products = renderer.step(render_products={PRODUCT}, delta_time=1 / 60, ordinal=1)<br>for product in products.values():<br>    for frame in product.frames:<br>        var = frame.render_vars[f&quot;{PRODUCT}/LdrColor&quot;].map(device=ovrtx.Device.CPU)<br>        pixels = np.from_dlpack(var).copy()   # copy before unmapping<br>        var.unmap()<br>        Image.fromarray(pixels).save(&quot;render.png&quot;)</pre><p>That is render_one.py. On my machine it takes about 50 s, almost all of it renderer start-up.</p><h3>2. Keep the scene loaded</h3><p>In the service, the worker keeps the scene populated and swaps only the product when the next job asks for the same OpenUSD file with another camera. The product must be added as a <em>reference</em>: publishing a product-only layer over the stage replaces the scene instead of adding to it.</p><pre>def show(self, req) -&gt; int:<br>    if req.force_reload or self.scene != asset_path(req.usd_path):<br>        if self.ordinal:<br>            ovstage.population.reset_usd(self.stage)      # don&#39;t merge with the old scene<br>        ordinal = self.publish()<br>        ovstage.population.open_usd_from_string(self.stage, scene_layer(req.usd_path), ordinal=ordinal)<br>        self.stage.advance_write_floor(ordinal)<br>        self.scene = asset_path(req.usd_path)<br>    if self.product is not None:<br>        ovstage.population.remove_usd(self.stage, self.product)<br>    self.product = ovstage.population.add_usd_reference_from_string(<br>        self.stage, product_layer(req.camera_path, req.width, req.height), &quot;/&quot;)<br>    ordinal = self.publish()<br>    ovstage.population.apply_usd_changes(self.stage, ordinal=ordinal)<br>    self.stage.advance_write_floor(ordinal)<br>    return ordinal</pre><p>After show, call renderer.reset() so accumulation starts fresh for the new view, then capture as in step 1. The worker checks a cancel flag between warm-up frames, the only safe point to stop.</p><h3>3. One worker thread, one job store</h3><p>The worker is a threading.Thread that creates the renderer <em>inside</em> run(), so every GPU call happens on that thread. The job store is a dictionary of jobs plus a heapq ordered by (−priority, submission number), guarded by a threading.Condition so the worker sleeps until a job arrives.</p><h3>4. The HTTP layer</h3><p>FastAPI handlers only touch the store: POST /render validates the request with Pydantic and queues it, GET /jobs/{id} reports status, and GET /jobs/{id}/result returns the PNG with FileResponse. create_app() starts the worker, and uvicorn.run() serves the app on 127.0.0.1:8000.</p><h3>Run and test</h3><p>Install and start the service from PowerShell:</p><pre>uv sync<br>uv run python render_service.py --port 8000 --output-dir _output</pre><p>The first start compiles shaders. GET /health reports renderer_ready: true once the worker has its renderer. Then render with the included client, or with any HTTP tool:</p><pre>uv run python client.py C:\scenes\robot.usda /CameraFront -o front.png</pre><pre>curl -X POST http://127.0.0.1:8000/render -H &quot;Content-Type: application/json&quot; \<br>  -d &#39;{&quot;usd_path&quot;: &quot;C:/scenes/robot.usda&quot;, &quot;camera_path&quot;: &quot;/CameraFront&quot;, &quot;width&quot;: 1024, &quot;height&quot;: 1024}&#39;<br># {&quot;job_id&quot;: &quot;f0a4e56b…&quot;}<br>curl http://127.0.0.1:8000/jobs/f0a4e56b…            # poll until &quot;done&quot;<br>curl -o front.png http://127.0.0.1:8000/jobs/f0a4e56b…/result</pre><p>I tested it with a KUKA KR 270 robot (42 meshes, SimReady materials, millimetre units) and three cameras at 1024 × 1024: one perspective and two orthographic. Orthographic cameras need nothing special in the service. Set the projection on the camera, and give the apertures in tenths of a scene unit, so 28000 covers 2.8 m in a millimetre stage:</p><pre>cam = UsdGeom.Camera.Define(stage, &quot;/CameraTop&quot;)<br>cam.AddTransformOp().Set(Gf.Matrix4d().SetTranslate(Gf.Vec3d(580, -7, 6000)))  # looks down -Z<br>cam.CreateProjectionAttr(&quot;orthographic&quot;)<br>cam.CreateHorizontalApertureAttr(28000.0)   # 2800 mm view width<br>cam.CreateVerticalApertureAttr(28000.0)<br>cam.CreateClippingRangeAttr(Gf.Vec2f(1, 20000))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/537/1*iL8d9I4JJi4YQtq63Wb9dQ.png" /><figcaption>Cameras in the Scene</figcaption></figure><p>The first job’s time depends on what the renderer has already cached: 1.5 s in a long-running environment, 13 s on the second start of a new one, and about 40 s on the very first.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jfnn0xGiApajpO5fPbqqZA.png" /><figcaption>Render Result</figcaption></figure><h3>Gotchas</h3><ul><li><strong>The first start is slow.</strong> In a fresh environment, creating the renderer took about 3 minutes (shader compilation) and the first job about 40 s. After that the shader cache is warm: renderer start-up drops to well under a minute, a new scene loads in seconds, and more views of it take half a second.</li><li><strong>Seal every ordinal.</strong> A publication that isn’t sealed with advance_write_floor makes the next step fail with “ordinal N exceeds ovstage write floor”.</li><li><strong>Reset before reloading.</strong> Opening a second scene without reset_usd merges it with the first one.</li><li><strong>Products as references.</strong> Publishing a product-only layer over a populated stage replaces the scene and renders black. Add the product with add_usd_reference_from_string, remove the old one, and apply_usd_changes.</li><li><strong>Output keys changed in 0.5.</strong> Render outputs are keyed by the full prim path, for example /RenderSvc/Product/LdrColor, not by the bare name.</li><li><strong>Unlit scenes.</strong> CAD exports rarely carry lights. The composed layer adds a dome light so they don’t render black.</li><li><strong>Cancellation is cooperative.</strong> A running job can only stop between warm-up frames.</li><li><strong>Shell.</strong> Start it from PowerShell or cmd, not Git Bash (see above).</li></ul><p>Ideas for next steps: prefer queued jobs for the scene that is already loaded, add a WebSocket for status updates instead of polling, and prune old PNGs from the output folder.</p><h3>Resources</h3><p>The complete sample (render_one.py, render_service.py, client.py, pyproject.toml) is in <a href="https://github.com/perfectproducts/openusd_studierstube/tree/main/examples/ovrtx_render_service">examples/ovrtx_render_service</a>.</p><p><em>This article and the sample code were written with the help of Claude Opus 5 (Anthropic) in Claude Code.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=be4fec701da7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Industrial Digital Twin Semantics in OpenUSD: Bringing AAS Submodel Templates into the Scene Graph]]></title>
            <link>https://mtw75.medium.com/industrial-digital-twin-semantics-in-openusd-bringing-aas-submodel-templates-into-the-scene-graph-1b877ea88ab0?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/1b877ea88ab0</guid>
            <category><![CDATA[digital-twin]]></category>
            <category><![CDATA[physical-ai]]></category>
            <category><![CDATA[industrial-ai]]></category>
            <category><![CDATA[openusd]]></category>
            <category><![CDATA[digital-product-passport]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Sat, 25 Apr 2026 20:37:49 GMT</pubDate>
            <atom:updated>2026-04-25T20:41:09.885Z</atom:updated>
            <content:encoded><![CDATA[<p><em>OpenUSD is rapidly becoming the common language for industrial digital twins. The AAS Submodel Template library offers something valuable: well-defined, industry-vetted semantic schemas for real-world assets. This article shows how to bring those two things together — and what it takes to do it cleanly.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tK7n3gmhMjOTbqh4oqj0Nw.png" /><figcaption>Mapping AAS Submodel Templates in OpenUSD</figcaption></figure><h3>OpenUSD as a Digital Twin Substrate</h3><p>OpenUSD is not a 3D format. It is a composable semantic scene graph for representing, simulating, and linking complex systems across domains. If that framing is new to you, we covered it in depth in our previous article — <a href="https://mtw75.medium.com/openusd-is-not-a-3d-format-c63506a318ab"><em>OpenUSD Is Not a 3D Format</em> </a>— and it is the foundation everything here builds on.</p><p>The short version: an OpenUSD stage can carry geometry, physical properties, sensor data, compliance metadata, and lifecycle records in a single composable, layered format. Applied API schemas attach typed, discoverable properties to any prim. Relationships create explicit graph edges between prims. The composition engine — with its LIVERPS arc ordering — assembles data from multiple sources non-destructively.</p><p>That infrastructure is mature and widely adopted. The question for industrial digital twins is not whether to use OpenUSD. The question is what semantic content to put in it.</p><h3>The Asset Administration Shell: Valuable Semantics, Difficult Architecture</h3><p>The Asset Administration Shell (AAS) was developed within Germany’s Industrie 4.0 initiative as the standardized digital twin framework for manufacturing. Backed by substantial public funding and institutional support from organizations such as Plattform Industrie 4.0 and the Industrial Digital Twin Association (IDTA), it defined a rigorous metamodel for describing industrial assets: their identity, their technical properties, their lifecycle history, their documentation.</p><p>In practice, AAS adoption has been limited. Even in Germany, where investment was heaviest, industrial deployments have remained largely confined to research and pilot contexts. Outside Germany, uptake has been close to negligible. The reasons are well understood in the industry: the AAS architectural stack is complex, the tooling ecosystem is thin, and the technology choices have a distinctly academic feel compared to the pragmatic, ecosystem-driven approaches that have succeeded at scale in software infrastructure.</p><p>The most useful part of AAS is its Submodel Templates (SMTs).</p><p>SMTs are standardized, versioned, semantically typed schemas for recurring industrial concerns: digital nameplates, technical data sheets, handover documentation, carbon footprints, predictive maintenance records, bill of materials. Each is developed by domain working groups and published in the IDTA content hub. In my experience, they vary in maturity and depth — but the strongest ones represent genuine, hard-won agreement across manufacturers, regulators, and system integrators about what information a particular type of object should carry and what it means.</p><p>That semantic precision is exactly what a general-purpose scene graph format like OpenUSD needs when it moves into regulated industrial contexts. OpenUSD does not know what a Digital Product Passport requires. AAS Submodel Templates do.</p><p>The opportunity is to use the SMTs as semantic source material for OpenUSD schemas — taking the well-defined vocabulary and regulatory grounding of AAS data definitions and expressing them as typed, discoverable, composable OpenUSD applied schemas.</p><h3>The Identifier Problem That Has to Be Solved First</h3><p>Before any AAS semantic content can be cleanly encoded in OpenUSD, there is a foundational conflict to resolve.</p><p>OpenUSD addresses every object in a scene by a <strong>namespace path</strong> — a hierarchical string like /Plant/Line3/Pump_CM5_3. These paths are the keys of OpenUSD&#39;s composition engine. They follow strict grammar rules. They must not contain characters that external systems routinely use in identifiers: slashes in the middle of a name, leading digits, hyphens used as separators, URI schemes.</p><p>Industrial assets carry identifiers from external systems that do not respect OpenUSD’s grammar: product URIs from regulatory registries, serial numbers from MES systems, order codes from ERP systems, document IDs from DMS platforms, facility identifiers under EU regulations. A battery’s product URI under EU Battery Regulation might be https://www.example.com/batteries/CM5-3/2024. An ECLASS-coded semantic identifier looks like 0173-1#02-AAO677#002. Neither can become an OpenUSD prim name without transformation — and transformation means data loss and broken cross-system traceability.</p><p>A recent OpenUSD Enhancement Proposal, <a href="https://github.com/asluk/OpenUSD-proposals/tree/aluk/source-identifiers"><strong>Separation of Concerns for Identifiers in OpenUSD</strong></a>, led by Aaron Luk and currently a draft submitted to the OpenUSD Alliance, addresses this directly. The principle is simple: OpenUSD namespace paths are for scene composition and hierarchy navigation. External identifiers should be stored <em>alongside</em> them, verbatim, in a standardized mechanism that is independent of OpenUSD grammar and discoverable by tooling.</p><p>Two candidate mechanisms are under evaluation — extending the existing assetInfo metadata dictionary with structured sub-dictionaries, or defining typed applied schemas with named properties. The mappings in this work use a sourceId:* attribute namespace (e.g., sourceId:dpp:uriOfTheProduct, sourceId:doc:documentId) as the practical expression of this principle: external identifiers are stored as plain strings on the prim, alongside OpenUSD-native typed attributes that encode the same data in OpenUSD-idiomatic form where applicable.</p><p>Without this mechanism, any AAS-to-OpenUSD mapping that involves external identifiers — which is essentially all of them — must choose between data loss and ad-hoc workarounds. The identifier separation is the enabling layer that makes clean mappings possible.</p><h3>The Mapping Approach</h3><p>Our mappings follow the <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/conceptual_data_mapping_template.md"><strong>OpenUSD Conceptual Data Mapping Template</strong></a>, a community document that provides a consistent structure for cross-format mappings. Each submodel mapping document specifies:</p><ul><li><strong>Directionality</strong>: one-way import or full bidirectional round-trip</li><li><strong>High-level concept table</strong>: AAS elements mapped to OpenUSD constructs, with round-trip fidelity rated per field (lossless, lossy, partial, or dropped)</li><li><strong>Per-concept drilldowns</strong>: encoding rules, fallback strategies, and data transformations</li><li><strong>Illustrative OpenUSD schema definitions</strong>: typed API schema classes formalizing the mapping</li><li><strong>Round-trip analysis</strong>: explicit documentation of what survives and what is lost in an AAS → OpenUSD → AAS cycle</li></ul><p>The general encoding pattern is consistent across all three submodel templates mapped so far:</p><ol><li><strong>The prim</strong> represents the physical or logical asset. Its OpenUSD namespace path is chosen for composition purposes, independent of any AAS identifier.</li><li><strong>Applied API schemas</strong> carry the submodel data as typed, discoverable properties. Tooling can find all prims carrying battery nameplate data by checking for BatteryNameplateAPI — no metadata parsing required.</li><li><strong>Source identifier attributes</strong> store external identifiers verbatim on the prim. They are distinct from the OpenUSD-native attributes that encode the same data in OpenUSD-idiomatic form.</li><li><strong>Child scope prims</strong> handle structured sub-elements with variable cardinality — markings, documents, document versions — using a consistent Concept_NN zero-padded naming convention that preserves AAS list ordering.</li><li><strong>customData</strong> preserves AAS-specific metadata for round-trip fidelity: semantic IDs, language variants, and original AAS idShort values.</li></ol><p>The OpenUSD schema hierarchy mirrors the AAS inheritance hierarchy. DppNameplateAPI is the shared base schema covering the full IDTA 02006-3-0 Digital Nameplate fields. Domain-specific schemas compose on top: BatteryNameplateAPI adds the five battery-specific fields from IDTA 02035-1; IndustrialEquipmentAPI adds the industrial equipment extensions. A battery prim carries both DppNameplateAPI and BatteryNameplateAPI — parallel to how the AAS Battery Nameplate SMT extends the Digital Nameplate SMT.</p><h3>The Three Submodel Templates Mapped</h3><h3>IDTA 02006–3–0 — Digital Nameplate for Industrial Equipment</h3><p>The Digital Nameplate is the foundational AAS SMT for industrial asset identity. It defines the core fields present in virtually every product passport: product URI, manufacturer name, product designation, order codes, article numbers, serial number, date of manufacture, facility identifier, markings, and asset-specific properties. Domain-specific nameplates (battery, machinery, software) derive from it.</p><p>A critical distinction the mapping makes explicit is between <strong>type-level</strong> and <strong>instance-level</strong> identifiers:</p><ul><li><strong>Type-level identifiers</strong> — ManufacturerProductType, OrderCodeOfManufacturer, ProductArticleNumberOfManufacturer — identify a <em>product model</em>: a class of equipment manufactured to a specification, shared across all units.</li><li><strong>Instance-level identifiers</strong> — SerialNumber, URIOfTheProduct, UniqueFacilityIdentifier — identify a <em>specific serialized unit</em>: a particular physical object at a particular location.</li></ul><p>This distinction is fundamental in AAS modeling and must survive in the OpenUSD encoding. Downstream systems — maintenance platforms, compliance registries, recycling operators — need to know whether they are looking at a model reference or a unit reference. Both classes of identifiers use the sourceId:dpp:* dual-encoding pattern, but the semantic distinction is preserved in the schema documentation and is available to tooling.</p><p>The idta_02006_digital_nameplate_sample.usda file shows a complete encoding for an industrial pump (Pump_MusterAG_CM5_3) including contact information, markings, ATEX explosion safety data, and asset-specific technical properties.</p><h3>IDTA 02035–1 — Digital Battery Passport (Part 1: Digital Nameplate)</h3><p>The battery passport nameplate is the Digital Nameplate extended for compliance with EU Battery Regulation (EU) 2023/1542. It adds five fields to the 02006–3–0 base: LifeCycleStage, OperatorIdentifier, ManufacturerIdentifier, EUDeclarationOfConformity, and ResultsOfTestReportsProvingCompliance.</p><p>In OpenUSD these are encoded in BatteryNameplateAPI, applied alongside DppNameplateAPI. ManufacturerIdentifier is an opaque external string stored in sourceId:dpp:manufacturerIdentifier. EUDeclarationOfConformity and ResultsOfTestReportsProvingCompliance are arrays of document identifier strings that cross-reference compliance documents in a Handover Documentation submodel — they are stored as sourceId:dpp:* string arrays, and the Handover Documentation mapping defines how to resolve the cross-reference on a shared OpenUSD stage. LifeCycleStage uses an OpenUSD token that maps to an ECLASS IRDI via a lookup table, preserving both OpenUSD-native enumeration semantics and AAS semantic identifier round-trip fidelity.</p><p>An important point about scope: the battery passport nameplate covers <em>identity and markings</em> only. The full DPP as defined by the EU Battery Regulation also requires technical data, carbon footprint, state of health, and other submodels. The nameplate mapping is the foundation layer; further submodel mappings build on it using the same schema composition approach.</p><p>The idta_02035_battery_passport_sample.usda file shows a complete battery instance (Battery_A12345X75EN).</p><h3>IDTA 02004–2–0 — Handover Documentation</h3><p>Handover Documentation is the AAS SMT for transferring product-related documents across organizational boundaries over an asset’s lifecycle: operating manuals, certificates, drawings, compliance declarations. Its central identifier challenge is the DocumentIds SML — an explicit multi-system identifier store where a single document may simultaneously carry IDs from a DMS, an ERP system, a regulatory body, and a certification authority. All of these must survive cross-system transit verbatim.</p><p>The mapping defines five coordinated API schemas: HandoverDocumentationAPI as the discoverer, DocumentAPI per document entry with OpenUSD relationships for intra-stage coverage, DocumentVersionAPI for file references and version metadata, DocumentIdAPI for individual document identifiers with sourceId:doc:documentId as the verbatim string, and DocumentClassificationAPI for VDI 2770 / IEC 61355 classifications.</p><p>The cross-reference with the battery passport is made explicit: when EU Declaration of Conformity IDs stored in sourceId:dpp:euDeclarationOfConformityIds on a battery prim match docId:documentId values in DocumentId_NN entries on the same stage, the link can be expressed as an OpenUSD relationship — connecting the regulatory compliance requirement on the product prim to the actual document record in the documentation layer.</p><p>The idta_02004_handover_documentation_sample.usda demonstrates this with two documents: an EU Declaration of Conformity cross-referenced from the battery passport, and an operating manual with German and English versions linked as translations.</p><h3>What This Enables and What Comes Next</h3><p>These three mappings demonstrate the methodology. The AAS SMT library contains many more: Technical Data (IDTA 02003), Carbon Footprint (IDTA 02023), Software Nameplate (IDTA 02007), Predictive Maintenance (IDTA 02017), and others. Each can follow the same pattern: map the SMT fields to typed OpenUSD applied schema properties, encode external identifiers verbatim via sourceId:* attributes, preserve semantic IDs and language metadata in customData, and define child prim structures for variable-cardinality sub-elements.</p><p>The result is an OpenUSD stage that can carry the full semantic richness of an AAS-defined asset description — with the geometry, physics, and simulation data that AAS has no vocabulary for — in a single composable, layered format that OpenUSD-native tooling can traverse, validate, and render without an AAS runtime.</p><p>OpenUSD does not need to become AAS. But it can carry what AAS Submodel Templates know how to define.</p><h3>Try the Files</h3><p>The mapping documents and USDA samples are available in the identifier_separation_of_concerns directory of the OpenUSD proposals repository:</p><p><strong>Mapping documents and samples:</strong></p><ul><li>Identifier Separation of Concerns proposal: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/README.md">README.md</a></li><li>Reusable mapping template: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/conceptual_data_mapping_template.md">conceptual_data_mapping_template.md</a></li><li>IDTA 02006–3–0 Digital Nameplate mapping: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02006_digital_nameplate_openusd_mapping.md">idta_02006_digital_nameplate_openusd_mapping.md</a></li><li>IDTA 02006–3–0 industrial pump sample: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02006_digital_nameplate_sample.usda">idta_02006_digital_nameplate_sample.usda</a></li><li>IDTA 02035–1 Battery Passport mapping: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02035_battery_passport_openusd_mapping.md">idta_02035_battery_passport_openusd_mapping.md</a></li><li>IDTA 02035–1 battery unit sample: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02035_battery_passport_sample.usda">idta_02035_battery_passport_sample.usda</a></li><li>IDTA 02004–2–0 Handover Documentation mapping: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02004_handover_documentation_openusd_mapping.md">idta_02004_handover_documentation_openusd_mapping.md</a></li><li>IDTA 02004–2–0 handover documentation sample: <a href="https://github.com/perfectproducts/OpenUSD-proposals/blob/feature/STW-0_article/proposals/identifier_separation_of_concerns/idta_02004_handover_documentation_sample.usda">idta_02004_handover_documentation_sample.usda</a></li></ul><p><strong>Official IDTA specifications:</strong></p><ul><li>Digital Nameplate for Industrial Equipment: <a href="https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2025/10/IDTA-02006-3-0-1_Submodel_Digital-Nameplate.pdf">IDTA 02006–3–0 (PDF)</a></li><li>Digital Battery Passport Part 1: <a href="https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2026/02/IDTA-02035-1_DBP-Part-1_Digital-Nameplate.pdf">IDTA 02035–1 (PDF)</a></li><li>Handover Documentation: <a href="https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2025/07/IDTA-02004-2-0_Submodel_Handover-Documentation.pdf">IDTA 02004–2–0 (PDF)</a></li></ul><p>The USDA files are valid USD ASCII and can be opened in any OpenUSD-capable tool. Schema classes are defined inline for illustration; in production they would be registered as proper OpenUSD schema plugins.</p><h3>In Summary</h3><p>AAS Submodel Templates contain something genuinely useful: semantically precise, industry-vetted definitions for real-world asset data, grounded in regulatory requirements and developed by domain experts. OpenUSD contains something AAS lacks: a proven, high-performance, widely-adopted composition and interchange infrastructure backed by the most significant technology investment in the 3D and simulation space.</p><p>The mappings presented here show how to take the valuable part of AAS — its submodel semantics — and express it cleanly in the format that is actually winning. The identifier separation proposal is the enabling mechanism that makes the encodings clean rather than lossy. The bidirectional mapping structure ensures that data can move in both directions without silent loss.</p><p>Industrial digital twins need good semantics. They also need infrastructure that the industry will actually use. These two things do not have to be in tension.</p><p><em>The mapping documents in this article are informed by the OpenUSD Enhancement Proposal “Separation of Concerns for Identifiers in USD,” led by Aaron Luk and currently a draft under review by the Alliance for OpenUSD.</em></p><p><em>The IDTA Submodel Template library is published at </em><a href="https://industrialdigitaltwin.org/en/content-hub/submodels"><em>https://industrialdigitaltwin.org/en/content-hub/submodels</em></a><em>.</em></p><p><em>This article was written with the assistance of </em><a href="https://claude.ai/code"><em>Claude Code</em></a><em> (Claude Sonnet 4.6) by Anthropic.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b877ea88ab0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OpenUSD Is not Another 3D Format]]></title>
            <link>https://mtw75.medium.com/openusd-is-not-a-3d-format-c63506a318ab?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/c63506a318ab</guid>
            <category><![CDATA[digital-twinstechnology]]></category>
            <category><![CDATA[omniverse]]></category>
            <category><![CDATA[digital-product-passport]]></category>
            <category><![CDATA[innovation]]></category>
            <category><![CDATA[openusd]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Sat, 25 Apr 2026 15:43:10 GMT</pubDate>
            <atom:updated>2026-06-11T11:57:29.530Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>It is a composable semantic scene graph for representing, simulating, and linking complex systems across domains. Born at Pixar, it is widely mistaken for a rendering format. Let’s correct that — and show why it matters for Digital Twins, Digital Product Passports, and Physical AI.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H7VdqpG48NbrLDFkEJKUgg.png" /><figcaption>OpenUSD Is not a 3D Format</figcaption></figure><p>It is easy to understand how the misconception formed.</p><p>Universal Scene Description was created at <a href="https://www.pixar.com/">Pixar</a>. Its first public showcase was a film pipeline tool. Its file extensions are .usd, .usda, .usdc, .usdz. <br>Its ecosystem features DCC tools — Maya, Houdini, Blender — and rendering workflows at Apple, NVIDIA, and Adobe.</p><p>If you encountered OpenUSD for the first time through any of those entry points, &quot;3D format&quot; is a reasonable first impression.</p><p>But OpenUSD has moved far beyond that origin. Today it is actively adopted in industry, construction, and robotics. NVIDIA’s Omniverse platform has been the primary force pushing OpenUSD into industrial contexts — factory simulation, robotic teleoperation, digital factory twins — and is now central to Physical AI workflows where simulation and the real world must stay in sync.</p><p>Global industrial software leaders like Siemens and PTC are adopting OpenUSD as part of their digital twin and PLM strategies. The format that assembled CGI scenes at Pixar is now assembling digital representations of production lines, buildings, and autonomous systems.</p><p>So why does the “3D format” label persist? Because the mental model never caught up with the technology.</p><h3>What OpenUSD Actually Is</h3><p>OpenUSD is best understood as a <strong>composable, layered scene graph and data model for representing complex systems</strong> — with geometry, time, relationships, and semantics.</p><p>Not just 3D.</p><p>It sits closer to:</p><ul><li>A graph-based digital twin substrate</li><li>A scene + data + relationship model</li><li>A composition system for heterogeneous industrial data</li></ul><p>The formal definition from the Alliance for OpenUSD captures it well: <br>a “rich and extensible ecosystem of software and standards for collaboratively constructing animated 3D scenes.”</p><p>But notice what that definition does not say: it does not say <strong><em>only</em> </strong>3D scenes.</p><p>The word “extensible” is doing a lot of work. The schemas that make OpenUSD meaningful for geometry — UsdGeomMesh, UsdGeomCamera — are just schemas. You can define any schema. You can represent anything.</p><h3>The Correct Mental Model</h3><p>Instead of:</p><blockquote><em>❌ OpenUSD = 3D rendering format</em></blockquote><p>The accurate model is:</p><blockquote><strong><em>OpenUSD = Scene Graph + Composition Engine + Typed Data Model + Relationship Layer</em></strong></blockquote><p>Each component matters independently:</p><p><strong>Scene graph</strong> — a hierarchy of objects. Not meshes. Objects. A battery cell, a compliance record, a robot joint, and a triangle mesh are all equally valid prim candidates.</p><p><strong>Composition engine</strong> — OpenUSD defines a precise set of composition arcs, remembered by the acronym <strong>LIVERPS</strong>: <strong>L</strong>ocal, <strong>I</strong>nherits, <strong>V</strong>ariantSets, r<strong>E</strong>locates, <strong>R</strong>eferences, <strong>P</strong>ayloads, <strong>S</strong>pecializes. These arcs resolve in a defined strength order, enabling non-destructive assembly of data from multiple sources and teams — overrides, configurations, deferred loading, and structural inheritance all composing cleanly without conflict.</p><p><strong>Typed data model</strong> — schemas and attributes with declared types. battery:capacity = 75.0 (kWh) is as valid as xformOp:translate = (1, 0, 0).</p><p><strong>Relationship layer</strong> — named, typed graph edges between prims. A sensor stream relationship, a supplier relationship, a simulation input binding — these are first-class OpenUSD constructs, not hacks.</p><p>Take away the geometry schemas and OpenUSD is still a complete, functional system. That is not an accident.</p><h3>Why It Is Fundamentally a Graph</h3><p>OpenUSD describes a directed scene graph:</p><ul><li><strong>Nodes</strong> → primitives (Prims)</li><li><strong>Edges</strong> → relationships (references, inherits, payloads, explicit rel declarations)</li><li><strong>Attributes</strong> → typed metadata on nodes</li></ul><p>Consider this:</p><pre>Car<br> ├── Body<br> │    ├── Material: Aluminum<br> │    └── Mass: 120 kg<br> ├── Battery<br> │    ├── Capacity: 75 kWh<br> │    └── Supplier: DID:example:supplier:123<br> └── Wheels<br>      ├── Geometry<br>      └── SensorData</pre><p>Nothing in this structure requires rendering. It is a <strong>semantic graph of a system</strong> — its composition, its properties, its provenance. The Geometry on Wheels is one attribute set among several. The SensorData on Wheels is equally valid OpenUSD.</p><p>This is where the mental model shift matters. A 3D format is for describing surfaces. OpenUSD is for describing systems.</p><h3>Where Semantics Live in OpenUSD</h3><p>OpenUSD has four distinct semantic mechanisms, and only one of them involves geometry.</p><p><strong>Prim structure (hierarchy)</strong> defines system decomposition:</p><p>product → subsystem → component → part</p><p><strong>Custom attributes (typed metadata)</strong> carry domain data:</p><pre>battery:capacity = 75.0<br>battery:recycledContent = 0.42<br>battery:cellChemistry = &quot;NMC&quot;</pre><p><strong>Relationships</strong> create graph edges beyond parent-child hierarchy:</p><pre>rel materialBinding<br>rel supplier<br>rel simulationInput<br>rel sensorStream</pre><p><strong>Applied schemas</strong> are where domain meaning gets encoded formally. Unlike typed schemas that define what a prim <em>is</em>, API schemas define what a prim <em>carries</em> — typed properties and behaviors declared once in a schema definition file, then applied to any prim via apiSchemas metadata.</p><p>Domain concepts like these become first-class schema types:</p><pre>class &quot;BatteryNameplateAPI&quot; (inherits = &lt;/APISchemaBase&gt;) { ... }<br>class &quot;DrivetrainAPI&quot; (inherits = &lt;/APISchemaBase&gt;) { ... }<br>class &quot;SensorBindingAPI&quot; (inherits = &lt;/APISchemaBase&gt;) { ... }</pre><p>Once registered via the usdGenSchema toolchain, these schemas generate typed accessors and make domain properties formally declared and discoverable — laying the groundwork for interoperability, with the degree of validation depending on the tooling built around them.</p><h3>How This Maps to Digital Product Passports</h3><p>The EU Battery Regulation (2023/1542) mandates a Digital Product Passport for batteries entering the European market: lifecycle data, material composition, recycled content, carbon footprint, compliance certifications.</p><p>This is not a rendering problem. It is a data, structure, and relationship problem.</p><p>OpenUSD can represent the structural and relational model of a battery product precisely because it is a graph — not a 3D format. A battery is not a mesh; it is a system. Its hierarchy of subsystems, its typed regulatory attributes, and its relationships to suppliers, certifications, and lifecycle records map directly to native OpenUSD constructs.</p><p>What emerges is a clean architectural separation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/930/1*uqEfrY2H12-qA73L5jpvQw.png" /></figure><p>Each layer solves a different problem.</p><p>OpenUSD does not define meaning — it structures it. It does not establish trust — it carries references to it. It does not handle data exchange — it provides the model being exchanged.</p><p>That distinction matters.</p><p>Treating OpenUSD as a rendering layer beneath a “real” data architecture leads to unnecessary complexity: duplicated models, fragmented semantics, and brittle integrations. The correct approach is to use OpenUSD as the structural substrate — the system model that everything else attaches to.</p><p>Digital Product Passports are not documents. They are graphs of products, properties, and relationships over time.</p><p>That is exactly what OpenUSD was built to represent.</p><p>In a companion article, we map the EU Digital Battery Passport to OpenUSD applied schemas in full — showing field by field how regulatory data lands cleanly in the scene graph, and what the bidirectional round-trip looks like in practice.</p><h3>Why OpenUSD Is Especially Powerful for Physical AI</h3><p>Physical AI — robots, autonomous systems, digital factories — requires a representation layer that can hold:</p><ul><li><strong>Multi-domain models</strong>: mechanical, electrical, software, simulation, sensor data, simultaneously</li><li><strong>Time and variants</strong>: configurations, states, versions — a robot arm in maintenance mode is a different variant of the same prim hierarchy</li><li><strong>Simulation integration</strong>: OpenUSD’s physics schemas (from NVIDIA’s PhysicsSchemas) connect directly to real-time simulation</li></ul><p>This is why OpenUSD is emerging as the structural backbone of Physical AI systems. It is not that game engines and robotics frameworks happen to use 3D formats for data exchange. It is that OpenUSD’s composition and schema system provides the right level of abstraction for representing complex, multi-domain physical systems — and geometry is simply one of those domains.</p><p>NVIDIA Omniverse is explicit about this: the platform is not a 3D viewer, it is a simulation and collaboration substrate. OpenUSD is its data model. Apple’s visionOS uses OpenUSD as its spatial computing format — not because Apple needed a 3D format, but because OpenUSD’s composition model handles the complexity of layered, multi-source spatial scenes better than anything else available.</p><h3>The One-Sentence Correction</h3><blockquote><strong><em>OpenUSD is not a 3D format — it is a composable semantic scene graph for representing, simulating, and linking complex systems across domains.</em></strong></blockquote><p>Its origins at Pixar gave it excellent geometry support. Its architecture gave it something more general: a principled, extensible, composable model for describing any structured system.</p><p>The geometry schemas are an application of OpenUSD, not its definition.</p><p>Treating OpenUSD as a 3D format in industrial and Physical AI contexts is a missed opportunity. Existing industrial semantics — AAS Submodel Templates, regulatory data models, PLM schemas — can be mapped directly to OpenUSD applied schemas, making them composable, layerable, and simulation-ready without rebuilding them from scratch. The composition engine does the heavy lifting; you just need to bring the right schemas to it.</p><p>Define the schemas. Use the relationships. Trust the composition engine. That is what OpenUSD is for.</p><p><em>This article was written with the assistance of </em><a href="https://claude.ai/code"><em>Claude Code</em></a><em> (Claude Sonnet 4.6) by Anthropic.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c63506a318ab" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Zero to Streaming Isaac Sim in 10 Minutes ]]></title>
            <link>https://mtw75.medium.com/from-zero-to-streaming-isaac-sim-in-10-minutes-cb09a3512477?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb09a3512477</guid>
            <category><![CDATA[isaac-sim]]></category>
            <category><![CDATA[synctwin]]></category>
            <category><![CDATA[nvidia]]></category>
            <category><![CDATA[digital-twin]]></category>
            <category><![CDATA[omniverse]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Sun, 07 Sep 2025 21:14:46 GMT</pubDate>
            <atom:updated>2025-09-07T21:14:46.673Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ReaTDlEGaNzdsZHvXzK3og.png" /><figcaption>NVIDIA Brev with IsaacSim</figcaption></figure><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fh5R1Saw6_o0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dh5R1Saw6_o0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fh5R1Saw6_o0%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/966c391493417cd99521daf596136a40/href">https://medium.com/media/966c391493417cd99521daf596136a40/href</a></iframe><p>Running Isaac Sim in the cloud often means wrestling with GPU drivers, configs, and networking. With <a href="https://developer.nvidia.com/brev"><strong>NVIDIA Brev</strong></a>, it’s almost plug-and-play.</p><p>I followed the <a href="https://docs.robotsfan.com/isaacsim/latest/installation/install_advanced_cloud_setup_brev.html">Isaac Sim Brev setup guide</a> and had a fully working streaming setup in about <strong>10 minutes</strong>. Here’s the flow:</p><p>1️⃣ <strong>Create a GPU instance</strong></p><ul><li>Sign in to Brev → <em>Create New Instance</em></li><li>Choose <strong>1× NVIDIA L40S GPU</strong></li><li>Deploy and wait until it’s ready</li></ul><p>2️⃣ <strong>Expose the streaming ports</strong></p><ul><li>Open ports <strong>49100</strong> and <strong>47998</strong> for WebRTC streaming</li></ul><p>3️⃣ <strong>Run Isaac Sim in Docker</strong></p><pre>docker pull nvcr.io/nvidia/isaac-sim:5.0.0<br>docker run --name isaac-sim --entrypoint bash -it --runtime=nvidia --gpus all \<br>  -e &quot;ACCEPT_EULA=Y&quot; -e &quot;PRIVACY_CONSENT=Y&quot; --rm --network=host \<br>  -v ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw \<br>  -v ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw \<br>  nvcr.io/nvidia/isaac-sim:5.0.0</pre><p>4️⃣ <strong>Start Isaac Sim livestreaming</strong></p><pre>PUBLIC_IP=$(curl -s ifconfig.me) &amp;&amp; \<br>./runheadless.sh --/app/livestream/publicEndpointAddress=$PUBLIC_IP \<br>                 --/app/livestream/port=49100</pre><p>5️⃣ <strong>Connect</strong></p><ul><li>Use the <strong>Isaac Sim WebRTC Streaming Client <br>(</strong><a href="https://docs.isaacsim.omniverse.nvidia.com/latest/installation/download.html#isaac-sim-latest-release">Download Isaac Sim — Isaac Sim Documentation</a>)</li><li>Point it to your instance’s public IP</li></ul><p>👉 And that’s it — from <strong>zero to streaming Isaac Sim in minutes</strong>, without painful setup.</p><p>This workflow makes NVIDIA Brev a great entry point for anyone who wants to explore Omniverse workloads quickly, whether for prototyping, testing, or scaling experiments.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb09a3512477" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How To Deploy a Virtual RTX Workstation and Omniverse Kit App in 20 Minutes]]></title>
            <link>https://mtw75.medium.com/how-to-deploy-a-virtual-rtx-workstation-and-omniverse-kit-app-in-20-minutes-a0e82cc580c6?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/a0e82cc580c6</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[openusd]]></category>
            <category><![CDATA[development]]></category>
            <category><![CDATA[synctwin]]></category>
            <category><![CDATA[omniverse]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Tue, 05 Aug 2025 21:47:22 GMT</pubDate>
            <atom:updated>2025-08-14T14:57:14.814Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vIIyF4cxqq9hZOv_PkN1fw.png" /><figcaption>USD Composer Template App</figcaption></figure><p>Setting up a local environment for Omniverse development can be a hassle — configuring GPU drivers, installing the right software versions, managing dependencies… you know the drill.</p><p>But what if you could skip all of that and launch a fully configured RTX-ready workstation in the cloud — in under 20 minutes?</p><p>Here’s how to do exactly that using AWS and NVIDIA’s Omniverse Workstation AMI. You’ll have a virtual machine ready for development and your first USD Composer Kit App running in no time.</p><iframe src="https://cdn.embedly.com/widgets/media.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D3LVh2oE4zZs&amp;type=text%2Fhtml&amp;schema=youtube&amp;display_name=YouTube&amp;src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F3LVh2oE4zZs%3Ffeature%3Doembed" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/0116949971a0c12f05a0202b0dba618f/href">https://medium.com/media/0116949971a0c12f05a0202b0dba618f/href</a></iframe><h3>🔧Step 1: Launch an Omniverse Workstation in AWS</h3><ol><li>Open the <strong>AWS EC2 Dashboard</strong> and click on <strong>“Launch instance”</strong>.</li><li>Under <strong>Amazon Machine Image (AMI)</strong>, search for “nvidia” and switch to the <strong>“AWS Marketplace AMIs”</strong> tab.</li><li>Choose either:<br>- <strong>NVIDIA Omniverse Development Workstation (Windows)</strong> — great for dev work, or<br>-<strong> NVIDIA Omniverse Workstation (Windows) </strong>— includes the Omniverse license ($1/hour)</li><li>Click Subscribe (or Subscribe on instance launch).</li><li>Select an instance type:<br>- Recommended: g6e.2xlarge (64 GB RAM, 8 vCPUs)<br>- Lightweight: g6e.xlarge (32 GB RAM, 4 vCPUs).</li><li>Create a new key pair and download the .pem file.</li><li>Click <strong>Launch instance</strong>.</li></ol><h3>🔐Step 2: Enable Remote Desktop Access (RDP)</h3><ol><li>In the instance summary, go to the <strong>Security </strong>tab.</li><li>Click the security group link, go to <strong>Inbound rules</strong>, and click <strong>Edit</strong>.</li><li>Add a rule for:<br>- Type: RDP<br>- Port: 3389<br>- Source: Anywhere — IPv4</li><li>Click <strong>Save rules</strong>.</li></ol><h3>🧩Step 3: Retrieve Your RDP Password</h3><ol><li>Select your running instance, click <strong>Connect</strong>, and go to the <strong>RDP Client </strong>tab.</li><li>Wait ~4–10 minutes for full initialization.</li><li>Click <strong>Get password</strong>.</li><li>Upload your .pem file and click <strong>Decrypt password</strong>.</li><li>Copy and save the password securely.</li></ol><h3>💻Step 4: Connect via RDP</h3><ol><li>Download the Remote Desktop (.rdp) file from the AWS Console.</li><li>Open it with your local RDP client and connect.</li><li>Enter the decrypted password.</li></ol><p>Congrats — you now have remote access to a fully equipped Omniverse development workstation! 🎉</p><h3>🧱Step 5: Create and Launch a USD Composer Kit App</h3><p>Now that you’re in, let’s build a sample Omniverse Kit App using the official template.</p><ol><li>Open a terminal and create a source folder, e.g., C:\_SOURCE</li><li>Clone the Omniverse Kit template:</li></ol><pre>git clone https://github.com/NVIDIA-Omniverse/kit-app-template.git</pre><p>3. Navigate into the project</p><pre>cd kit-app-template</pre><p>4. Run the template wizard:</p><p>.\repo.bat template new</p><p>5. Accept the terms, choose:</p><p>- <strong>Application</strong><br>- <strong>USD Composer</strong><br>- Press <strong>Enter </strong>for the remaining prompts to use default names.<br>- Select <strong>No </strong>for additional streaming layers.</p><p>6. Build &amp; Launch your new Kit app:</p><pre># build (once) <br>.\repo.bat build<br><br># launch <br>.\repo.bat launch</pre><p>Boom! Your own USD Composer Kit App is now running in a powerful RTX-accelerated virtual workstation in the cloud.</p><p>Whether you’re experimenting with Omniverse development or onboarding a new team member, this setup saves time and removes friction.</p><h3>Troubleshooting</h3><h4>Insufficient vCPU Quotas</h4><p>Your quota for vCPUs might not be sufficient</p><ul><li>go to Service Quotas -&gt; Dashboard and search for “on-demand”</li><li>apply for more vCPUs for “Running On-Demand G and VT instances” ( we need 8 per additional AMI ) .</li></ul><h4>No Access / Can’t connect with RDP</h4><p>Make sure to download the RDP file after you’ve started the machine — the IP adress will change each time you start it, so don’t reuse previously downloaded RDP files.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a0e82cc580c6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to use Hunyuan3D 2.1 GenAI with NVIDIA Omniverse on Windows]]></title>
            <link>https://mtw75.medium.com/how-to-use-hunyuan3d-2-1-genai-with-nvidia-omniverse-on-windows-6b2714e4c8b7?source=rss-b9afcb256636------2</link>
            <guid isPermaLink="false">https://medium.com/p/6b2714e4c8b7</guid>
            <category><![CDATA[omniverse]]></category>
            <category><![CDATA[industrial-metaverse]]></category>
            <category><![CDATA[openusd]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[hunyuan-tencent]]></category>
            <dc:creator><![CDATA[Michael T. Wagner]]></dc:creator>
            <pubDate>Mon, 30 Jun 2025 06:36:57 GMT</pubDate>
            <atom:updated>2025-06-30T06:36:57.955Z</atom:updated>
            <content:encoded><![CDATA[<p>In this guide, you’ll learn how to set up and run Hunyuan3D 2.1 — a generative AI model for 3D content creation — on Windows using Docker and NVIDIA Omniverse, including building a custom Omniverse KIT App to generate 3D models from images.</p><h3>Build &amp; Run Docker Container</h3><p>In order to use the API server we have to clone the <a href="https://github.com/perfectproducts/Hunyuan3D-2.1">repository </a>with the docker image description from github. It is a fork of the original <a href="https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1">repository </a>from Tencent with added API server.</p><p>Let’s build the docker image and start our container. Make sure to have <a href="https://docs.docker.com/desktop/setup/install/windows-install/">Docker Desktop</a> the <a href="https://learn.microsoft.com/en-us/windows/wsl/install">Linux on Windows subsystem</a> (WSL) installed.</p><p>Please note the <a href="https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1/blob/main/LICENSE">LICENSE</a> information from Tencent, which excludes the EU, UK and South Korea.</p><pre># checkout repository <br>git clone https://github.com/perfectproducts/Hunyuan3D-2.1.git . <br># build docker image <br>cd docker <br>docker build -t hunyuan3d21 .<br># run docker container with gpu support and exposed server port 8081<br>docker run --gpus all -p 8081:8081 hunyuan3d21<br>...<br># (first start takes around 45 minutes to download dependencies) <br>INFO:     Application startup complete.<br>INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)</pre><p>Make sure that the server is reachable by opening the documentation page of the server API and by executing the health operation:</p><p><a href="http://localhost:8081/docs#/status/health_check_health_get">http://localhost:8081/docs#/status/health_check_health_get</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/950/1*6MX22GmN8qeY5HwTgAbBkQ.png" /></figure><p>Once the server is running we are sure that every dependency has been downloaded so lets commit the state of the container to a new image which is ready to use on container startup.</p><pre># save our ready to use container to a new image <br>docker commit [your container name] hunyuan3d21:readytorun<br><br># here&#39;s how we run our new image <br>docker run -it --rm --gpus all -p 8081:8081 hunyuan3d21:readytorun<br><br></pre><p>Now we’re ready to build our own Omniverse KIT App !</p><h3>Build &amp; Run Omniverse KIT App &amp; Extension</h3><p>Let’s check out the source code and build the KIT app with our Hunyuan 3D Extension:</p><pre># clone repo <br>git clone https://github.com/perfectproducts/synctwin_hunyuan3d_ov .<br><br># build KIT app <br>repo build <br><br># run KIT app <br>repo launch</pre><p>Now we can launch the KIT App with our Hunyuan3D KIT extension.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MMJivgMG2FnhG1SgbgcsMg.png" /><figcaption>NVIDIA Omniverse KIT App with Extension</figcaption></figure><p>To try our extension we take an arbitrary image from a website — here we use the image of an industrial machine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/955/1*-f7FJSVKR6wQ6cZWHrRcPA.png" /><figcaption>website with picture of our desired model</figcaption></figure><ul><li>After we’ve saved the picture we select it by clicking on the image button (1)</li><li>Set the host and port of the docker container with the preferences button (2). The status line on the bottom of the widget should say (Online)</li><li>Click on “Generate 3D” button and wait for the generation of your 3D model. It should be loaded after generation (takes 1–2 minutes).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*btaNaf6Mutr6E8GsiWhkzw.png" /><figcaption>Extension UI Elements</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DmTz4mnz4apdhqu9MA1d7w.png" /></figure><p>Happy generating !</p><h3>Troubleshooting</h3><h4>Increase WSL Memory Limit</h4><p>If the server runs out of memory during generation you can assign more memory to the WSL.</p><p>Edit C:\Users\&lt;YourUsername&gt;\.wslconfig and increase the memory limit to more than 16GB to prevent crashes in the docker container due to out-of-memory. (for me 32 works)</p><pre>[wsl2]<br>memory=32GB<br>processors=4<br>swap=8GB</pre><h4>Add Firewall Rule for Port 8081</h4><p>Windows Defender Firewall blocks external access by default, even if Docker is exposing the port .</p><p>🔧 Fix:</p><pre>Press Win + R, type wf.msc, hit Enter.<br>Go to Inbound Rules → New Rule.<br>Choose Port → TCP → 8081<br>Allow the connection → Apply to all profiles.<br>Give it a name like “Docker App Port 8081” → Finish.</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b2714e4c8b7" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>