Compare commits

..

71 Commits

Author SHA1 Message Date
indigo 05201465be Validate persisted OCIO preferences against the active config
A display or view saved in preferences.ini that doesn't exist in the current
OCIO config made HdxColorCorrectionTask throw every frame
("DisplayViewTransform error. Display 'x' not found") and silently skip color
correction, which reads as a washed-out viewport -- worst on delegates like
hdEmbree whose output depends on the transform actually running.

The two are easy to invert: in the bundled ACES 1.2 config the only display is
"ACES" and "sRGB" is one of its views, but the shipped preferences had
OcioDisplay=sRGB and OcioView="ACES 1.0 SDR-video" (an OCIO-v2 studio-config
view that doesn't exist here). Nothing validated them -- LoadPreferences fed
ApplyGlobalColorCorrection directly, and the only existing fallback lived in
the preferences dialog and triggered on empty, never on invalid.

ValidateOcioPreferences() now runs right after LoadPreferences() so both the
viewport and the Material Editor see corrected values:
- display falls back to the config default when missing
- view is checked against the *resolved* display's view list, since correcting
  the display can invalidate the view; prefers the config default view when
  valid for that display, else its first view
- color space and look are cleared when they name something absent, as they
  fail the same way (note the real name is "ACES - ACEScg", not "ACEScg")
- no-op when the config is unreadable, so a missing $OCIO doesn't clobber
  values that may be correct for a config supplied later

Each correction logs a warning naming the old and new value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:24:24 +08:00
indigo a907ec0182 Add collapsible Sublayers section to Scene Hierarchy panel
Puts the sublayer stack directly under the edit-target combo in the Scene
Hierarchy, so muting and reordering no longer require switching to the
separate Stage Editor window.

Each row mirrors StageEditorPanel: drag handle with drag-drop reorder, dirty
indicator, name (dimmed when muted, [RL] badge for render-layer sublayers),
edit-target checkbox and mute checkbox. Adds dedicated up/down chevron icon
buttons for reordering, which the Stage Editor only exposed as context-menu
items. Reorder goes through the existing LayerReorderCommand so it is undoable;
mute calls LayerManager directly, matching current app-wide behaviour.

New Icon::ChevronUp / ChevronDown with matching SVGs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:03:05 +08:00
indigo b5c1e50438 Add Maya-style Render Layer panel
Render layers built on the existing USD sublayer architecture: each layer is a
file-backed SdfLayer mounted as a sublayer and tagged via custom layer data,
with membership stored in a UsdCollectionAPI on a hidden /RenderLayerData prim.
Activating a layer applies its overrides and isolates it to its members.
"Default" is virtual -- it means all render-layer sublayers muted.

See docs/adr/0002-render-layer.md for the design and rationale.

- RenderLayerManager: create/delete/rename/activate, membership, mute-based
  isolation. Sdf-level APIs are used while a layer is muted (Usd-level APIs
  can't see muted layers) and Usd-level APIs when active.
- Commands: create/delete/rename/activate and add/remove members, all undoable.
- RenderLayerPanel: layer list with one-click activation and a membership
  editor for the selected prims.
- StageEditorPanel / SceneHierarchyPanel: badge render-layer-owned sublayers,
  disable their mute/remove controls (they're managed from the Render Layer
  panel so the two can't desync), and lock the edit target while a non-Default
  layer is active.
- ConnectShaderAttrs/DisconnectShaderAttr: take an explicit editLayer captured
  at construction, so undo targets the same layer as execute even if the
  ambient edit target changed in between (e.g. a render-layer switch).

Note: SceneHierarchyPanel.h also carries declarations for the sublayers
section added in the following commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:02:33 +08:00
indigo e2a4961ebe Upgrade to OpenUSD 25.11 (Python 3.12) and fix Arnold/Embree regressions
Build against OpenUSD 25.11 built from source with Python 3.12, OCIO 2.2.1,
OpenVDB, Ptex and Embree, replacing the 25.05/py311 mix.

Build system:
- CMAKE_PREFIX_PATH -> third_party/OpenUSD-v25.11; HDARNOLD_ROOT ->
  hdArnold-v25.11. hdEmbree is bundled in the USD build now, so
  HDEMBREE_USD_ROOT is no longer needed.
- FindOpenUSD: drop usd_ndr (ndr was merged into sdr in 25.11).
- hdArnold plugin renames: ndrArnold -> nodeRegistryArnold, plus the new
  usdImagingArnold plugin.
- Glob the version-suffixed OpenColorIO_*.dll instead of hardcoding 2_1.
- Drop the python311.dll workaround; USD and Cycles now share Python 3.12.
- Cycles: deploy OpenColorIO_2_5.dll (needed by its bundled OpenImageIO) and
  stop the debug-DLL filter from eating IlmThread.dll -- the regex `d[.]dll$`
  also matched legitimate Release DLLs.

Runtime fixes:
- cullStyle now defaults to Nothing, matching usdview (viewSettingsDataModel
  cullBackfaces=False). BackUnlessDoubleSided drops back faces on geometry
  that isn't authored doubleSided, which hdEmbree applies to occlusion rays
  too, so interiors shaded as single-sided.
- Set HDARNOLD_osl_includepath (and PXR_MTLX_STDLIB_SEARCH_PATHS) in main.cpp
  before the plugin DLL pre-load. hdArnold compiles MaterialX via generated
  OSL that begins with #include "mx_funcs.h"; without an include path Arnold
  fails with "fatal error: 'mx_funcs.h' file not found". It must be set before
  any plugin loads because TF_DEFINE_ENV_SETTING caches the value when
  hdArnold.dll registers its settings.
- Deploy the MaterialX standard library next to the executable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:02:00 +08:00
indigo 9d61819636 Update README to reflect Material Editor, Timeline/Curve Editor, and OCIO color management
README hadn't been touched since the early layer/viewport-only stage of
the project: it still described the removed LayerPanel, omitted the
Material Editor / MaterialPreviewRenderer / NodeThumbnailCache /
CurveEditorPanel / TimelinePanel entirely, and its Tests section
claimed ctest works even though BUILD_TESTS is OFF in every preset
(the two test executables have stale USD include paths, per
CLAUDE.md). Refreshes the feature list, architecture tree, and build
docs to match current source, and adds a screenshot gallery for the
Material Editor, Curve Editor, quad viewport, and MaterialX import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:45:04 +08:00
indigo 27c619c518 Add screenshots verifying Arnold node thumbnail convergence fix
Material Editor with Arnold-shaded RustMetalMat: node thumbnails
(texture and standard_surface shader ball) now render converged
under the Arnold delegate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:39:56 +08:00
indigo 196a2bb0dd Fix node thumbnails not converging with progressive renderers; add colorSpace and Sdr-enum widgets
- NodeThumbnailCache/MaterialPreviewRenderer: keep a material node
  thumbnail stale (re-pumped each frame) until the shared preview
  renderer actually converges, instead of caching after one render
  pass. Progressive delegates (Arnold/Cycles/Embree) need multiple
  passes to accumulate past their first noisy sample, so thumbnails
  previously froze on an early frame and only "fixed themselves"
  when toggling thumbnail display forced a fresh render.
- MaterialEditorPanel: add a Color Space row under Asset-typed
  shader inputs, authoring the attribute's colorSpace metadata from
  the active OCIO config's color spaces.
- MaterialManager: surface Sdr-declared allowed values (enums) per
  shader input so the property panel can render a dropdown instead
  of free text.
- CMakePresets.json: build the release preset with debug info
  (RelWithDebInfo) for easier diagnosis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:39:14 +08:00
indigo 6cb1b45496 Delete unuse icon 2026-07-11 11:08:59 +08:00
indigo a877e8973c Add thumbnail toggle and set-all pin-display toolbar icons
Material editor toolbar gets an eye icon to hide/show node thumbnails
(skips the reserved space and decode/render work, not just the
visual) and a 3-dot icon that broadcasts a pin-display mode (All /
Connected / Collapsed) to every node in the open material at once.
The dot icon is shared with the per-node toggle via a parameterized
DrawPinDisplayModeIcon, with dot radius/spacing scaled to fit the
toolbar's fixed icon-button size instead of overflowing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 11:07:57 +08:00
indigo a7314ff108 Re-author eye, eye-slash, file-plus, and swatchbook icons in Inkscape
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 11:07:46 +08:00
indigo bff42082a5 Add Collapsed pin display mode with a 3-dot state icon
Cycle each node through All -> Connected -> Collapsed -> All via a
custom-drawn icon (filled-dot count encodes how much is shown: 3/2/1).
Collapsed hides every pin on a side, linked or not, behind one
connector nub; existing links now redirect to that nub too
(EffectivePinId), so they visually converge like Hypershade's collapse
view instead of only hiding unconnected pins. Also moved the connector
nubs off the title row onto the node's bottom row. Persisted mode
switches from a bool (uiShowAllPins) to an int (uiPinDisplayMode).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 01:52:52 +08:00
indigo c7edd145df Hypershade-style pin display modes for material graph nodes
Each node gets an All/Conn toggle: connected-only mode hides
unconnected pins behind collapsed connector nubs on the title row;
dragging a link onto (or from) a nub opens a menu to pick which hidden
pin to wire, and the pin appears once connected. The toggle is drawn
and hit-tested geometrically, and its tooltip is deferred to after
NE::End() so it lands at the cursor. A new Preferences > Material
checkbox opts into persisting the per-node mode as USD customData
(uiShowAllPins); otherwise it stays session-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:02:56 +08:00
indigo 22406fbe63 Fix ImGui hover dead zones at negative canvas coordinates
Port upstream imgui-node-editor's IMGUI_HAS_VIEWPORT support into the
vendored canvas: docking-branch ImGui's IsMouseHoveringRect requires
rects to overlap the mouse viewport's screen-space rect, so items at
canvas coordinates outside the screen range (e.g. negative) never
hovered or clicked. Transform the viewport into canvas-local space in
EnterLocalSpace() and restore it in LeaveLocalSpace(), like the mouse
position. Fixes node hover highlight and in-node widget interaction
left of / above the canvas origin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:02:44 +08:00
indigo d9826b7bbb Add in-canvas node thumbnails to material editor graph
Texture nodes show their decoded source image (background thread);
material nodes show a shader-ball render, amortized one-per-frame.
Also classifies shader nodes by Hypershade-style category and adds
an Arnold preset graph alongside the existing USD/MaterialX ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 15:08:56 +08:00
indigo 7d214821f5 Drag-to-create-and-connect nodes; double-click to rename materials
Drag a link off a pin onto empty canvas to open a create-node menu at
the drop point (the editor's QueryNewNode path), filtered to nodes with a
type-connectable pin; the chosen node is created and auto-wired to the
source pin. Direction is handled both ways — drag from an output makes a
downstream consumer, from an input an upstream producer. Type matching is
lenient (exact, or same scalar/2-/3-/4-vector class), mirroring what USD/
MaterialX networks already tolerate; the target pin is the first exact
match else the first connectable. RenderNodeSearchMenu gained an optional
linkSource arg so the TAB popup and this share one implementation, and
CreateShaderNode now returns the created path. Create and connect are two
undo steps.

Materials list: double-click a row starts an inline rename (mirrors
SceneHierarchyPanel — Enter commits, focus-loss/Esc cancels) through
RenamePrimCommand, which fixes up bindings/arcs via UsdNamespaceEditor.
Selection and the open graph follow the rename. This repurposes the row's
double-click, which previously opened the graph; opening stays on the
Show Graph button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:13:36 +08:00
indigo 2ddeeed061 Icon toolbar and resizable browser lists in material editor; mute debug logs
Toolbar: every action is now an icon button (tooltip carries the label,
text fallback with no icon set) via a local iconBtn helper matching the
StageEditor/Timeline convention — Create Material, Presets, Create + Bind,
Bind Current. Icons fill the standard button height (GetFrameHeight, zero
frame padding) so the row stays uniform.

Browser: the materials list and the create-node list are now split by a
draggable HorizontalSplitter (sibling to the column VerticalSplitter)
instead of a fixed 45% / separator; m_browserListRatio holds the list's
share (clamped 0.15-0.85). Create-node entries get a per-category glyph
(Material/Texture/Geometry/Light/Utility) sized to the text line height.

Silence the temporary node-editor drag-regression tracing: the
[NodeGraph] event-trace block is #if 0'd, its PersistNodePosition logs
commented, and the [ed] g_AxNodeEditorDebugLog hook install is commented
out (the vendored editor's dumps are guarded on that hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:16:04 +08:00
indigo 038450a1ca Vendor node editor in-tree; add material presets; debug drag regression
Move the imgui-node-editor subset the build actually compiles from
third_party/imgui-node-editor to src/ui/NodeEditor (version-controlled,
MIT LICENSE included). FindImguiNodeEditor.cmake points at the new
location; CMakeLists excludes src/ui/NodeEditor from UI_SOURCES so it
isn't compiled twice.

Material editor presets: a Presets menu (disabled with no open material)
builds a UsdPreviewSurface + UsdUVTexture graph fed by an st primvar
reader, or a MaterialX standard_surface + image graph fed by a texcoord
node. Each is one idempotent, undoable command that re-normalizes layout.
Create Material becomes an icon button. New nodes route through
FindFreeCanvasSpot so a creation never lands on top of an existing node
(overlapping nodes fight over the editor hit test and become undraggable).

Shader-ball preview: pick a previewable output (terminal or 3/4-component
color-like) instead of always the first output, so scalar-only nodes keep
the whole-material preview rather than failing Storm codegen. Per-shape
camera frame-fit margins and auto-clip framing.

Fixes: DeletePrimCommand::Undo recreates missing destination ancestors
before SdfCopySpec (parent material may have been deleted after the
command ran). ConfigWindowsMoveFromTitleBarOnly stops a content-area drag
in the node canvas from moving the whole Material Editor window.

Temporary (marked for removal once the node-editor drag regression is
diagnosed): main.cpp mirrors LOG_INFO to %APPDATA%\UsdLayerManager\
debug.log; a g_AxNodeEditorDebugLog hook in the vendored editor plus a
[NodeGraph] event-trace block in RenderNodeGraphCanvas dump click/drag/
selection/position-save state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:35:23 +08:00
indigo 0f30e080e3 Add preview shape picker to shader ball; drag controls for node properties
Preview geometry dropdown: Sphere / Cube / Cylinder (native prims),
Teapot and Hair (bundled resources/preview/*.usdc - teapot tessellated
from the Newell patch data with the Blinn 1.3x height correction, hair
as 220 procedural B-spline strands), and Cloud Volume (UsdVolVolume
over the OpenVDB bunny_cloud sample, downloaded at CMake configure and
gitignored like the HDRIs). Shapes live under one scope with the
material bound on it; switching activates one shape and reframes the
camera. Missing assets drop out of the dropdown. usdVol added to the
USD link set. The camera starts in a Maya-style 3/4 view.

Node property values switch from Input* fields back to draggable
controls (Ctrl+click to type): live-apply while dragging, one undo
command on release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:39:16 +08:00
indigo 6b8cb2406e Move preview renderer and HDR pickers above the shader ball
Drop the 'Shader Ball' label; the renderer switch and HDR environment
dropdowns sit side by side at the top of the preview column, above the
swatch. Both pickers take an optional item width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:26:39 +08:00
indigo c5ca6a7501 Show composition arcs and asset info in PropertyPanel
Prims with authored references or payloads get a section below the
prim header listing every list-op entry (explicit/add/prepend/append/
delete) with its asset path and target prim; a folder icon button
browses (USD/usdz/MaterialX/Alembic filter) and replaces the entry's
asset path, authored as one undoable whole-list-op swap into the edit
target. Prims with composed assetInfo get a read-only key/value section
(resolved paths shown in tooltips). PropertyPanel now receives the
IconManager for the browse button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:16:58 +08:00
indigo de94decf64 Persist material editor column widths in preferences
MaterialBrowserWidth / MaterialPreviewWidth in preferences.ini, saved
from the live splitter positions on shutdown and restored (clamped to
the splitter ranges) at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:54:06 +08:00
indigo 42fd355c53 Group create-node list by source and category; make editor columns resizable
The create-node list is now a two-level tree: a header per node source
(USD, MaterialX, Arnold, Cycles - from the Sdr sourceType, with glslfx
and OSL merged into USD so core nodes don't appear twice) containing
Hypershade-style category sub-trees (Material / Texture / Geometry /
Utility / Light) derived from Sdr context, terminal output types, role,
and name keywords. Duplicate collapsing is per (source, label) so
same-named nodes from different providers both stay; searching forces
all sections open. The TAB popup shows [Source/Category] per entry.

The browser and preview columns get drag splitters (viewport-divider
styling, clamped widths); the canvas absorbs the remainder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 06:47:12 +08:00
indigo b95b567b75 Require Shift to dock windows
io.ConfigDockingWithShift: plain title-bar/tab drags just move a
window; holding Shift shows the dock-position overlay and docks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
indigo bb9a5d9428 Author shader-ball node-preview overrides in the session layer
Fixes 'No spec at </Materials/...> when trying to set field
primChildren' during pin edits: the per-revision wipe used raw spec
surgery (RemoveNameChild/RemoveProperty) plus a ClearReferences/
AddReference cycle that briefly leaves the material's over spec empty
and inert, which Sdf cleanup can delete out from under later authoring.

The reference to the source material is now authored only when the
material changes (shared in-memory layers recompose live), and all
node-preview overrides live in the preview stage's session layer,
wiped wholesale with SdfLayer::Clear — no spec-level removal remains,
and switching materials no longer leaves stale overrides behind. The
edit target is pinned to the root layer so the rig never lands in the
cleared session layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
indigo b08bb6b233 Style material editor node properties like PropertyPanel
Same table anatomy as the attribute inspector: authored-state (a)
mini-button with path/type tooltip and copy-path popup, fixed name
column, stretch value column with row striping. Widgets switch to the
same vocabulary (InputFloat/InputScalarN %.4f, InputInt, default
ColorEdit swatches) committing on deactivation, and connected inputs
render as '-> node.output'. Preview column widens to 320 to fit; the
shader ball centers in it. Per-edit undo commands are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:09:05 +08:00
indigo 09819091c4 Add Hypershade-style Material Editor with node graph and shader-ball preview
Browser column lists all scene materials plus a searchable create-node
list; Show Graph (or double-click) loads a material into the node-graph
work area. The graph shows the entire network including MaterialX
(.mtlx) node graphs, resolving connections through NodeGraph boundaries
via UsdShadeUtils::GetValueProducingAttributes, with layered auto-layout
for nodes lacking authored uiPosition. Canvas navigates viewport-style
(Alt+MMB pan / Alt+RMB zoom) and TAB opens a Nuke-style search popup.
Selecting a node shows a typed property editor (live-apply, one undo
command per edit) and previews that node's output on the shader ball.

The preview renders through its own Hydra engine into a scratch stage
that composes the material via a reference to the source root layer (so
referenced .mtlx materials work), re-renders until progressive delegates
(Arnold/Cycles/Embree) converge, and lights with HDR dome presets
(External/Room/Interior/Sunset; CC0 Poly Haven EXRs fetched at CMake
configure). texture:format is authored latlong explicitly - left
automatic, hdArnold falls back to Arnold's angular fisheye default -
and hdCycles gets a -90 X pole rotation to match Storm's +Y-pole
sampling.

Mutations go through new ICommand subclasses (create shader node,
connect/disconnect attrs). imgui-node-editor is vendored (gitignored)
with a local one-line patch: c_ScrollButtonIndex 1->2 for MMB pan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:44:54 +08:00
indigo 338970b243 Mask viewport split dividers under floating windows
The divider hit-zones are raw screen rects; gate them on
IsWindowHovered(ChildWindows) so a window stacked over the viewport
(e.g. the Material Editor) doesn't highlight or start a resize drag
through it. In-progress drags still track until release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:44:36 +08:00
indigo 0640201d5b Add keyable attrs, clear override, material binding, and API schema groups to PropertyPanel
Attributes with SdfVariabilityVarying now show a (k) button when the
timeline is at a non-default frame. Orange = exact time sample at the
current frame; gray = no sample here. Clicking keys the current value
via AttributeSetCommand (undoable).

Right-click on any attribute or relationship name opens a context menu
with "Clear Override" (greyed when nothing is authored at the edit
target). Undo restores all previously authored time samples and default
values.

Prims with UsdShadeMaterialBindingAPI now show a "Material Binding"
collapsing section above the transform block, listing Direct / Preview /
Full purpose bindings and the resolved computed material path.

RenderPropertiesTable now groups attributes and relationships under
CollapsingHeader sections named after their USD API schema. Base-type
attributes (e.g. Mesh, Xform) appear first; applied API schema groups
(MaterialBindingAPI, CollectionAPI, etc.) follow alphabetically.
Uncategorised attributes are bucketed by namespace prefix (primvars,
inputs, outputs, …). xformOp attrs are still filtered out (shown in the
TRS section above).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 07:13:21 +08:00
indigo f7a2262e80 Add multi-select, specifier column, and visibility shortcuts to hierarchy panel
Ctrl+click toggles prim selection; Shift+click range-selects using a
per-frame visible-order list. Multi-selection is broadcast to the viewport
via a new SetOnPrimsSelected callback so Hydra highlights all selected prims.

Adds a 5th table column showing each prim's SdfSpecifier (def/over/class)
with colour coding. 'over' prims are shown in orange to make overrides
immediately visible.

Visibility icon updated: invisible prims show EyeSlash in orange instead
of grey. Ctrl+H / Shift+H keyboard shortcuts hide / show all selected prims.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 07:04:22 +08:00
indigo 781d464f47 Add per-viewport cull style option
Exposes UsdImagingGLCullStyle as a Cull Style sub-menu in the gear popup
(Back Unless Double-Sided / Back / Front / Nothing / No Opinion). Defaults
to BackUnlessDoubleSided, matching usdview. Setting persists per-viewport
to viewport_settings.ini.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:15:20 +08:00
indigo 7586d380e9 Move Purpose toggles into a Purpose sub-menu in the gear popup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:21:31 +08:00
indigo 1c55907573 Add renderer purpose display toggles (Guide, Proxy, Render)
Exposes UsdGeomImageable purpose visibility as three per-viewport toggles
in the gear popup (Purpose: Guide / Proxy / Render), wired to
showGuides/showProxy/showRender on UsdImagingGLRenderParams. Defaults match
the USD render params defaults (Proxy on, Guide and Render off). Settings
persist per-viewport to viewport_settings.ini.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:02:35 +08:00
indigo 205bf65ab3 Add renderer settings dialog with tab categories per delegate
Surfaces each render delegate's settings (FLAG/INT/FLOAT/STRING) in a
per-viewport floating dialog opened from the gear popup. Settings are grouped
into tabs by key namespace prefix (split at ':'); unprefixed keys go into
General. Changes apply immediately; Reset Defaults restores defValues.
Settings list is cached per delegate to avoid TF_WARN flooding the log on
every frame.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 04:34:53 +08:00
indigo 862f3c9745 Add per-viewport AOV selection (stageView.py GetRendererAovs port)
Exposes the render delegate's available AOVs in the gear popup under a new
"AOV" sub-menu. Selecting an AOV calls SetRendererAov on the engine and
persists the choice per-viewport in viewport_settings.ini. Switching the
render delegate resets the AOV to "color", matching stageView.py's
_handleRendererChanged. Menu is greyed-out when the delegate reports no AOVs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 03:03:00 +08:00
indigo 6adf022c13 Add Camera Guide viewport toggle (stageView.py DrawCameraGuides port)
Adds a per-viewport "Camera Guide" toggle (default off) that draws a full
frustum wireframe box (near quad + far quad + 4 connecting edges) in oracle
orange for every non-active UsdGeomCamera prim, matching usdview's
displayCameraOracles behaviour. Uses GfFrustum::ComputeCorners() and the
same 24-index line layout as DrawCameraGuides. Toggle exposed in the
right-click context menu and gear popup; persists to INI as ShowCameraGuide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 02:35:39 +08:00
indigo a59b3a5405 Replace custom ViewportColorCorrector with stageView.py color correction
Remove the custom GL post-process (ViewportColorCorrector) that bypassed
HdxColorCorrectionTask. Instead pass colorCorrectionMode, ocioDisplay,
ocioView, ocioColorSpace, and ocioLook directly to UsdImagingGLRenderParams
and SetColorCorrectionSettings(), delegating correction to Hydra —
the same approach used in usdview's stageView.renderSinglePass().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 01:35:06 +08:00
indigo 45d1cc4391 Add screen shot for MaterialX display 2026-06-29 08:25:19 +08:00
indigo cb0d72d980 Add global Preferences dialog and per-viewport render delegate
Preferences (Edit > Preferences, tabbed General/Animation/Viewport):
- Viewport tab: Default Render Delegate and Color Correction settings
- Persisted to %APPDATA%\UsdLayerManager\preferences.ini
- Render delegate: startup default only (not live-applied to running tiles)
- Color correction: global, applied live to all viewports on change

Render delegate per-viewport:
- Each tile toolbar switches its own delegate independently
- Saved per-tile in viewport_settings.ini; restored on next launch
- Tiles with no saved delegate get the pref default on startup

Color correction global:
- Removed from gear popup; lives in Preferences > Viewport only
- Applied to all tiles on init and on live change from Preferences

Fix InitRenderer always applying m_currentRendererPlugin when set,
rather than skipping when UsdImagingGLEngine auto-selects a default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:43:32 +08:00
indigo b02a736da0 Fix prim expiry crashes and add context menu hierarchy ops
Crash fixes (expired UsdPrim after namespace edits):
- RenderPrimNode: early-out after rename commit (renamed flag + column cleanup)
- RenderPrimNode: early-out after context menu op expires prim (prim.IsValid() guard)
- RenderContextMenu: EndPopup+return when Unparent/Group expires prim mid-popup
- Child iteration: snapshot GetChildren() into vector before recursing to guard
  against live iterator invalidation from stage mutations deeper in the tree
- Rename: firstFrame guard prevents spurious IsItemDeactivated cancel on
  SetKeyboardFocusHere activation frame (fixes second-rename not working)
- RenamePrimCommand: check RenamePrim() return value, log if rejected

New features:
- Context menu Add Child submenu: create typed child prim under selection
- Context menu Unparent to Root: move prim to stage root (undo-able)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 03:43:01 +08:00
indigo b31ca69d4f Add Maya-style prim editing to SceneHierarchyPanel
- Inline rename: double-click prim label or F2 to edit name in-place;
  Enter commits, click-away or Esc cancels (undo-able)
- Drag-and-drop reparent: drag any prim onto another to make it a child;
  invisible root drop zone below tree to reparent to stage root (undo-able)
- Ctrl+G group: creates Xform group at common parent, reparents selection
  under it; filters descendant duplication; selects new group (undo-able)
- Delete key shortcut: opens existing remove-prim modal when panel focused
- Context menu: Rename (F2) and Group (Ctrl+G) items added
- Three new undo-able commands using UsdNamespaceEditor (USD 25.05):
  RenamePrimCommand, ReparentPrimCommand, GroupPrimsCommand

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:44:55 +08:00
indigo 88ae8ccefa Update CLAUDE.md 2026-06-27 18:41:41 +08:00
indigo 46d83d300d Add ADR for custom viewport color correction
Document the decision to replace HdxColorCorrectionTask with a custom GL
post-process (linear render + sRGB/OCIO correction via the OCIO GPU API),
including rationale, pipeline, consequences, and alternatives considered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:46 +08:00
indigo fb1ae9d3a6 Add custom viewport color correction (sRGB/OCIO), bypassing HdxColorCorrectionTask
Hydra's HdxColorCorrectionTask rendered prims black in OCIO mode and could
corrupt the GlfDrawTarget bind stack on failure (skipping Unbind), blacking
out every later frame including sRGB. Replace it with our own GL post-process:
the scene renders linear (RGBA16F) and is corrected by a fullscreen shader --
linear->sRGB encode, or OCIO via the OCIO 2.1 GPU API (GpuShaderDesc plus
uploaded 1D/3D LUT textures). OCIO build failures fall back to sRGB (never
black) and USD diagnostics are routed to the app log.

- core: ViewportColorCorrector + ApplyViewportColorCorrection in UsdSceneRenderer
- utils: OcioConfigParser enumerates displays/views/colorspaces/looks from $OCIO
- ui: gear-menu OCIO controls (ViewportTile) + per-viewport persistence (ViewportPanel)
- Application: point $OCIO at the bundled ACES 1.2 config
- CMake: link/copy OpenColorIO, download ACES 1.2 config; plus hdCycles build
  config (disable OpenVDB/Embree, fix TBB/OpenSubdiv/Imath dirs, exclude CRT DLLs)
- main: pre-flight plugin DLL load check to skip plugins with missing deps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:10:44 +08:00
indigo 8316e419ba Add gear icon toolbar button with viewport options dropdown
Adds a ⚙ icon button to the right of the AA toggle in each viewport
tile's compact toolbar.  Clicking it opens a dropdown with:
  - Shading submenu: Smooth Shaded / Flat Shaded / Wireframe on Surface /
    Wireframe / Unlit  (new ShadingMode enum driving UsdImagingGLDrawMode)
  - Lighting submenu: Camera Headlight / Scene Lights + Dome Light toggle
  - Show Grid and Anti-Aliasing toggles (mirrored from icon buttons)
  - Bounding Box submenu: None / Per Object / All Selection
  - Background submenu: Dark Gray / Black / Light Gray / Midnight Blue

ShadingMode is persisted in viewport_settings.ini alongside the existing
per-tile settings (saved on shutdown, restored on startup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 09:56:49 +08:00
indigo 9d8840ced6 Add per-viewport render settings persistence (delegate, grid, AA, bg, bbox, lighting)
ViewportTileSettings struct captures: render delegate, show grid, AA, background
color, bbox mode, ambient-light-only, dome-light flags.

ViewportPanel::SaveSettings() writes a simple INI file (layout + one [TileN]
section per tile) to %APPDATA%\UsdLayerManager\viewport_settings.ini on
Application::Shutdown().

ViewportPanel::LoadSettings() is called at the end of Application::Initialize()
— after the viewport panel is constructed but before the first frame — so the
saved render delegate is picked up by UsdSceneRenderer::InitRenderer() on the
first Render() call.  Layout (Single/HSplit/VSplit/Quad) and split ratios are
also restored, creating the correct number of tiles before settings are applied.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 09:34:00 +08:00
indigo 4b7323c24c Fix hdCycles DLL init failure in install: exclude bundled CRTs, add python311.dll
Two independent fixes for the install layout:

1. Cycles bundles old MSVC CRT DLLs (MSVCP140 v14.34, VCRUNTIME140 v14.34)
   that are placed in install/bin/ via the CYCLES_INSTALL_DIR copy. These take
   precedence over System32 on the DLL search path. sycl8.dll (built with newer
   VS2022 CRT v14.42) calls functions that don't exist in the old version, causing
   ERROR_DLL_INIT_FAILED (1114) when hdCycles.dll loads embree4 -> sycl8. Fix:
   exclude MSVCP140*.dll, VCRUNTIME140*.dll, ucrtbase*.dll, api-ms-win-*.dll from
   the install(DIRECTORY cycles_install/) rule. The system VC++ Redistributable
   satisfies these at runtime.

2. usd_python.dll (the OpenUSD Python bindings DLL) was compiled against Python
   3.11 while our build targets Python 3.12. Add auto-detection of python311.dll
   from common install paths and deploy it to both the build output and install/bin/
   so the dependency chain hdCycles->usd_python->python311 is satisfied.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 09:07:38 +08:00
indigo e85944ccb6 CMake install: fix Hydra plugin path and include all built delegates
plugin/usd without a trailing slash made CMake install the directory
itself under bin/usd/, landing plugins at bin/usd/usd/hdStorm/ etc.
(one level too deep). Adding the trailing slash installs the contents
directly into bin/usd/, matching the build-directory layout that
POST_BUILD produces via copy_directory.

Also removed the now-redundant install(FILES hdStorm.dll DESTINATION bin)
which was placing the DLL in the exe root instead of bin/usd/ where
plugInfo.json's LibraryPath expects it.

hdEmbree, hdArnold, and hdCycles install rules were already correct
(they explicitly target bin/usd/); this change makes the base USD
plugin/usd contents consistent with them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 20:42:52 +08:00
indigo ab934d0818 Save imgui.ini to %APPDATA%\UsdLayerManager\ instead of CWD
Sets io.IniFilename to %APPDATA%\UsdLayerManager\imgui.ini right after
CreateContext so ImGui never touches the working directory for layout
persistence. The directory is created on first run via create_directories.
m_iniFilePath on ImGuiContext holds the string so the pointer stays valid
for the lifetime of the context.

Also removes imgui.ini from version control and adds it to .gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 20:36:25 +08:00
indigo 08f4bd351b Add Screen Shot for Render Delegate 2026-06-24 09:34:20 +08:00
indigo ddffa5764c Curve editor: fix repeated Apply after bake
Three bugs prevented Apply from working correctly after the first bake:

1. m_needsRefresh spuriously set during bake
   ClearAllTimeSamples/WriteChannelValue fire OnObjectsChanged synchronously.
   Since the channel is dirty at that moment, it was skipped, setting
   anyMatchedChannel=false and triggering m_needsRefresh=true. This caused
   RefreshFromStage() on the next frame, clearing m_selection and rebuilding
   m_channels — dropping the user's edit context. Fixed with m_suppressNotice:
   set true during USD writes so OnObjectsChanged is a no-op during bake.

2. Prim-level metadata notice triggered rebuild
   SaveBezierToMetadata calls SetCustomDataByKey, which fires a notice with
   changed==primPath. No attribute channel matched, so m_needsRefresh=true
   again. Fixed in OnObjectsChanged: skip changed==primPath (metadata-only
   changes), and mark a path as "known" even if the channel is dirty (so
   the refresh gate only fires for genuinely new/unknown attributes).

3. Dangling chPtr in undo/redo closures
   After RefreshFromStage() cleared and rebuilt m_channels, the raw pointer
   captured in closures pointed to freed memory. Replaced with a findCh
   lambda that searches m_channels by attr path + component at call time.
   Falls back to m_needsRefresh=true if the channel no longer exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:28:09 +08:00
indigo e8a73674ad Curve editor: fix context menu, persist Bezier metadata, always-visible handles
Context menu fix:
- Move RMB popup and hit-test lambdas before the early-return guard in
  HandleCanvasInput so BeginPopup is called every frame while the popup is
  open, regardless of whether the mouse has left the canvas area.
  Previously the early-return fired (hovered=false) the moment the mouse
  moved onto the menu, silently closing it before any item could register.

Bezier metadata persistence:
- On Apply/bake, write each channel's BezierKey array into
  prim.customData["curveEditor"]["channels"][attrName@component] as flat
  double[]/int[] arrays (times, values, inDt, inDv, outDt, outDv, broken).
- FitCurveFromSamples tries LoadBezierFromMetadata first; falls back to
  Catmull-Rom only when no saved entry exists, so tangent shapes survive
  file save/reload.
- Bake undo command snapshots the entire curveEditor customData value and
  restores it on Ctrl+Z, keeping metadata in sync with sample history.

Always-visible tangent handles:
- Remove keySel guard so in/out handle lines and circles render for every
  keyframe, not just selected ones.
- hitTestHandle now iterates all visible channels/keys so any handle can
  be clicked directly without first selecting its parent keyframe.
- Clicking a handle auto-selects the parent key as well, keeping context
  menu operations (Flatten, Break, Delete) consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:07:01 +08:00
indigo e0597ae31d Add screen shot for curve editor 2026-06-24 08:26:13 +08:00
indigo 425d21db45 Add animation curve editor with Bezier F-curve round-trip
Maya-style Graph Editor panel (View > Curve Editor) that fits Catmull-Rom
Bezier curves through USD time samples, allows interactive editing with
tangent handles, and bakes the result back to dense linear samples on Apply.

Key behaviours:
- Left panel lists all animated attributes of the selected prim, decomposed
  into per-component channels (translate [X/Y/Z], etc.) with colour swatches
  and visibility toggles
- ImDrawList canvas: smooth Bezier polylines, always-visible tangent handle
  lines/circles, diamond keyframe markers
- Pan (MMB/Alt+drag), zoom (scroll / Shift+scroll), Frame All (F)
- LMB drag moves keyframes; tangent handle drag reshapes curve with mirrored
  or broken handles; box-select for multi-selection
- Double-click canvas adds a keyframe; Delete removes selected keyframes
- RMB context menu: Delete, Flatten, Break/Unify Tangents, Auto Tangents
- Simplify toggle (Ramer-Douglas-Peucker) reduces baked sample count
- Time cursor draggable to scrub the timeline
- UsdNotice::ObjectsChanged listener re-fits clean channels whenever the
  stage changes externally (Property Panel edits, Auto-Key, undo/redo),
  while preserving channels with unsaved Bezier edits
- Bake is a single undoable AttributeSetCommand (Ctrl+Z restores original
  sparse samples); Revert discards in-editor edits without touching USD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 08:25:25 +08:00
indigo 0538dd3243 Add playblast: viewport capture to H.264 MP4 via libavcodec
- TimelinePanel: Playblast button opens modal with output dir (native
  folder picker via IFileOpenDialog), custom resolution (HD/FHD/4K
  presets), frame range, and movie export toggle
- UsdSceneRenderer: CaptureFrame() reads pixels from resolved MSAA FBO;
  Render() stores last dimensions for off-screen re-render at capture res
- MovieEncoder: streams RGBA frames directly into MP4 using libavcodec/
  libswscale (H.264, tries libx264 → nvenc → qsv → amf → openh264)
- Application: CapturePlayblastFrame() re-renders at capture resolution
  each frame; opens/writes/closes MovieEncoder inline with the render loop
- FileDialog: BrowseFolder() using Vista-style IFileOpenDialog (COM)
- CMake: FindFFmpeg.cmake locates prebuilt BtbN GPL shared package in
  third_party/ffmpeg; links and copies DLLs for main and test targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 09:36:30 +08:00
indigo c796c76561 Renderer: suppress camera headlight when stage contains authored lights
Mirrors usdview behaviour -- when the stage has at least one UsdLux
light the default camera fill-light is disabled so the scene is lit
purely by its authored lights.  StageHasAuthoredLights() early-exits
on the first HasAPI<UsdLuxLightAPI>() hit to keep the per-frame cost
minimal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 08:17:10 +08:00
indigo 2649fe814c Add light shape wireframe display in viewport
Draw a type-appropriate wireframe gizmo for every UsdLux light in the stage,
mirroring the existing camera-wireframe overlay (dedicated VAO/VBO reusing the
bbox shader, drawn on top with depth-test off).

Shapes, built in the light's local space and pushed through its local-to-world
transform:
  - SphereLight   3 orthogonal circles (radius); axis-cross when treatAsPoint
  - RectLight     width x height rectangle + emission direction line
  - DiskLight     circle + direction line
  - CylinderLight two end-cap circles + connectors (radius/length)
  - DistantLight  sun disc + parallel rays
  - DomeLight     3 large display-scaled circles

Selected lights draw accent orange, others warm yellow.

Verified by creating a SphereLight (3-circle sphere) and a RectLight
(rectangle) in-app: distinct shapes, correct selected/deselected colours,
gizmos visible over geometry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 07:56:47 +08:00
indigo a24eff04bd CMake: drop invalid OPTIONAL from Cycles runtime-deps install
install(DIRECTORY) disallows OPTIONAL together with FILES_MATCHING, which
broke reconfigure. The directory is always present here (produced by the
CyclesBuild step), so OPTIONAL was unnecessary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:49:29 +08:00
indigo 4d3cc85ece Fix rotate manipulator desync on already-rotated prims
The matrix->Euler decode at the end of a rotate drag used the rows of the
prim's initial rotation matrix (its already-rotated frame) as the
DecomposeRotation axes. But UsdGeomXformCommonAPI::SetRotate interprets the
written angles about the CANONICAL x/y/z axes, so the two only agreed when
the prim was at zero rotation -- the authored orientation desynced from the
manipulator the moment the prim already carried a rotation.

Decompose the result matrix about the canonical axes instead, matching how
SetRotate reconstructs it. The result-matrix computation (which correctly
tracks the visual ring axis) is unchanged.

Verified numerically via the real XformCommonAPI authoring path: from
rotateXYZ=(30,45,60), a +20deg object-X drag now authors exactly
(50,45,60) (err 0.0) vs the old decode's (41.07,27.07,66.61) (err 0.287).

Assumes RotationOrderXYZ (the XformCommonAPI default); non-XYZ orders are
mapped as before -- noted in a code comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:49:22 +08:00
indigo 119a55b1aa docs: add renderer version requirements to render delegate table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:30:34 +08:00
indigo 3210c16c7f docs: update README with render delegate support and Cycles build notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:22:54 +08:00
indigo 7b1fe91cac CMake: add hdCycles Hydra delegate build and deployment
- Build Cycles from third_party/cycles via ExternalProject (WITH_CYCLES=ON)
- Deploy hdCycles.dll + plugin metadata to usd/ alongside hdEmbree/hdArnold
- Copy Cycles runtime deps (embree4, openvdb, OIDN, sycl, imath, etc.) via
  a cmake -P script that globs at build time, excluding CRT DLLs to avoid
  MSVCP140 version conflicts
- Deploy tbb12.dll explicitly from the precompiled lib tree (required by
  openvdb/embree4 but absent from the install output)
- Bake all renderer paths into CMakePresets.json default preset
  (WITH_CYCLES, HDARNOLD_ROOT -> E:/library/hdArnold, ARNOLD_LOCATION,
  HDEMBREE_USD_ROOT) so a fresh configure needs no manual -D flags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 10:08:01 +08:00
indigo 81f9e9b4e4 CMake: add hdArnold plugin detection and deployment
Adds HDARNOLD_ROOT and ARNOLD_LOCATION cache variables to detect and deploy
the arnold-usd Hydra render delegate (hdArnold.dll, ndrArnold.dll) built from
arnold-usd Arnold-7.4.0.0 against MtoA 5.5.0. DLLs land in usd/ alongside
their plugInfo metadata dirs so LibraryPath ../hdArnold.dll resolves correctly.
ai.dll is copied to the exe root for Windows DLL search resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:22:52 +08:00
indigo 364973fb65 CMake: fix hdEmbree.dll deployment path -- copy to usd/ not exe root
plugInfo.json declares LibraryPath '../hdEmbree.dll', relative to usd/hdEmbree/,
so the DLL must live at usd/hdEmbree.dll. The previous POST_BUILD copied it to
the exe root, causing the module-not-found error at load time.

Fix: copy hdEmbree.dll into usd/ for POST_BUILD and install(). embree3.dll
stays in the exe root where Windows DLL search finds it as a dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:45:50 +08:00
indigo 11ce024d6c CMake: support separate HDEMBREE_USD_ROOT for hdEmbree integration
The previous approach assumed hdEmbree lived inside OpenUSD_ROOT_DIR, which
only works when USD itself was built with PXR_ENABLE_EMBREE_PLUGIN=ON.

This change adds HDEMBREE_USD_ROOT to point at a separate USD build that
includes hdEmbree (e.g. E:/USD_v25.05_embree), while the main USD install
remains at OpenUSD_ROOT_DIR. Fixes three bugs from the prior commit:

- Detection now resolves _hdembree_root from HDEMBREE_USD_ROOT first
- hdEmbree.dll path corrected: it lives at plugin/usd/hdEmbree.dll, not
  inside the plugin/usd/hdEmbree/ metadata subdirectory
- DLL glob changed from tbb12.dll (Embree 4) to embree*.dll only; tbb*.dll
  is deliberately excluded to avoid conflicts with the main USD install
- POST_BUILD now also deploys the hdEmbree/ metadata dir to usd/hdEmbree/
- install() deploys the hdEmbree/ metadata dir alongside hdEmbree.dll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:40:47 +08:00
indigo 91fff304b5 CMake: add hdEmbree plugin detection and deployment
When the USD installation contains plugin/usd/hdEmbree/ (i.e. USD was built
with PXR_ENABLE_EMBREE_PLUGIN=ON), the build system now:
- Detects OPENUSD_HAS_EMBREE and logs status in the CMake configure output
- POST_BUILD: copies Embree runtime DLLs to the exe directory (looks in USD lib/
  first, or EMBREE_LOCATION cache variable if DLLs live in a separate SDK dir)
- install(): deploys hdEmbree.dll and Embree DLLs alongside other runtime DLLs
- Adds EMBREE_LOCATION CACHE PATH for pointing at an external Embree SDK bin/

No app code changes needed: UsdImagingGLEngine::GetRendererPlugins() already
enumerates all registered Hydra plugins, and the per-viewport renderer dropdown
already lists them. HdEmbree will appear automatically once the plugin is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:36:28 +08:00
indigo d280136ca7 Modify font size to 15 2026-06-18 07:15:55 +08:00
indigo 81d4372bf6 Scene hierarchy: add Inherit, Specialize, and VariantSet composition arc menus
Prim right-click context menu now exposes all five USD composition arc types:

- Inherit▶  Add... (modal for SdfPath input) | Remove▶ (lists via GetAllDirectInherits()) | Clear All
- Specialize▶  Add... | Remove▶ (lists via UsdPrimCompositionQuery + ArcTypeFilter::Specialize) | Clear All
- VariantSet▶  Add... | per-set sub-menu: Add Variant... + selectable variant list (SetVariantSelection)

All text-input operations share a single RenderArcModal() driven by ArcModalMode enum.
VariantSet/Variant names go through SanitizeUsdName(); Inherit/Specialize paths are passed
as-is to SdfPath. OK button is disabled until input is non-empty; Enter key also confirms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 04:48:18 +08:00
indigo 3ba00761c1 Scene hierarchy: group composition arcs into Reference/Payload submenus + add Payload ops
- Prim context menu: consolidate Add/Replace/Remove Reference under a single Reference▶ submenu
- Prim context menu: add Payload▶ submenu with Add.../Remove▶/Clear All
- Stage blank-area context menu: same grouping (Reference▶ and Payload▶)
- Include pxr/usd/usd/payloads.h and pxr/usd/sdf/payload.h; use SdfPayloadEditorProxy + UsdPrimCompositionQuery::ArcTypeFilter::Payload for per-payload removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 04:37:16 +08:00
indigo f56f4674cc Property panel: token[] view/edit, uniform row heights
- Add token[] attribute display: inline InputText (or Combo when
  allowedTokens metadata is present), collapsible [N tokens] tree
  for arrays with up to 32 editable entries
- Force all property table rows to GetFrameHeight() minimum via
  TableNextRow min_row_height so text-only and widget rows match
- Add AlignTextToFramePadding() before every TextUnformatted /
  TextDisabled / TextColored in cols 0-2 so short text sits centred
  in the uniform-height row rather than pinned to the top
- Wrap matrix attributes in TreeNodeEx [NxN] so they collapse to a
  single-height row; relationship targets use inline text / collapsible
  tree instead of a variable-height ListBox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:02:41 +08:00
indigo 251174ddc3 Property panel: display and edit primvars:displayColor[] as color swatches
DrawVtValueWidget now handles Color-role array attributes (VtArray<GfVec3f>
and VtArray<GfVec4f>) that previously fell through to the generic [array N]
fallback.

- Single element: inline ColorEdit3/4
- Multiple elements: collapsible row with up to 5 swatch previews + [N] count;
  expanded view shows per-index ColorEdit3/4 (capped at 16 entries)
- Editing any element writes the full modified VtArray back via attr.Set()

Covers primvars:displayColor, primvars:displayOpacity[], and any other
color3f[]/color4f[] primvar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 07:28:04 +08:00
indigo dc0c15de70 Scale manipulator: center handle for uniform scale + hover fix
- Add center dot hit test (axis 3) for the scale gizmo so hovering/
  clicking the center circle performs uniform scale on all three axes.
  Center is checked before HitTestAxes to prevent the pivot (which is
  an endpoint of all three axis segments) from always resolving to X.

- Uniform scale uses screen-space pixel delta (right = grow, left =
  shrink, 200 px ≈ ×2) instead of the world-space axis-line ratio.
  The axis-line approach broke because the mouse starts at the pivot
  (oT ≈ 0), making cT/oT explode on any small movement.

- Center dot highlights in orange and grows when hovered or active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 06:47:42 +08:00
105 changed files with 27529 additions and 430 deletions
+9
View File
@@ -1,5 +1,6 @@
# Build directories # Build directories
build/ build/
build_*/
install/ install/
out/ out/
.kilocode/ .kilocode/
@@ -72,3 +73,11 @@ desktop.ini
# Temporary files # Temporary files
*.tmp *.tmp
*.temp *.temp
# ImGui layout (saved to %APPDATA%\UsdLayerManager at runtime)
imgui.ini
# OCIO config LUT files — downloaded automatically at CMake configure time
resources/OpenColorIO-Configs/
resources/hdri/
resources/vdb/
+73 -1
View File
@@ -1,4 +1,76 @@
# CLAUDE.md — 12-rule template # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
---
## Build
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
OpenUSD 25.11 is expected at `third_party/OpenUSD-v25.11` (set via `CMAKE_PREFIX_PATH` in the preset).
```powershell
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
cmake --preset default
# Build Release (copies all USD/DLL dependencies to build/Release/)
cmake --build build --config Release
# Install to install/bin/
cmake --build build --config Release --target install
```
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON` and `HDARNOLD_ROOT` for the maintainer's machine. hdEmbree is bundled directly in the `third_party/OpenUSD-v25.11` build (built with `--embree`), so no separate `HDEMBREE_USD_ROOT` is needed. For a minimal build without optional render delegates, configure manually:
```powershell
cmake -B build -G "Visual Studio 17 2022" `
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.11" `
-DIMGUI_DIR="third_party/imgui-1.92.7" `
-DWITH_CYCLES=OFF
```
**No test runner**`BUILD_TESTS=OFF` by default. Tests (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths; do not enable without fixing them first.
---
## Architecture
**Entry point:** `src/main.cpp` — sets `PXR_PLUGINPATH_NAME`, pre-flight loads each plugin DLL via `LoadLibraryA` (skips missing deps gracefully), then hands off to `Application`.
**Application** (`src/ui/Application.h/.cpp`) — owns all top-level managers and ImGui panels. Drives the main loop: `Update()``RenderUI()`. Persists viewport settings to `%APPDATA%\UsdLayerManager\viewport_settings.ini`.
**Core managers** (all in `src/core/`):
- `UsdStageManager` — opens/closes/saves USD stages; single source of truth for the active `UsdStageRefPtr`.
- `LayerManager` — enumerates and manipulates SdfLayer stack (mute, set edit target, add/remove sublayers).
- `PropertyManager` — reads/writes prim attributes; drives `PropertyPanel`.
- `CommandHistory` — undo/redo stack; all mutating operations go through commands in `src/core/commands/`.
**Viewport rendering pipeline** (the most complex subsystem):
- `ViewportPanel` (`src/ui/`) — container for 14 `ViewportTile` instances, handles split layout and shared selection.
- `ViewportTile` — owns one `ViewportCamera` + one `UsdSceneRenderer`. Renders into an ImGui child window by passing the scene texture as an ImGui image.
- `UsdSceneRenderer` (`src/core/`) — wraps `UsdImagingGLEngine` (Hydra/Storm). Renders into a `GlfDrawTarget` (offscreen RGBA16F FBO). All GL overlay drawing (grid, axis, bbox, camera/light wireframes) happens here while the draw-target FBO is bound.
- `ViewportColorCorrector`**file-local class inside `UsdSceneRenderer.cpp`** (pimpl). Applies sRGB or OCIO color correction as a fullscreen GL pass *after* Hydra renders linear. Hydra's own `HdxColorCorrectionTask` is bypassed (`colorCorrectionMode = "disabled"` always passed to Hydra). See `docs/adr/0001-viewport-color-correction.md`.
**OCIO config:** `$OCIO` is set at startup (in `Application.cpp`) to `resources/OpenColorIO-Configs/aces_1.2/config.ocio` (downloaded at CMake configure time). The `OcioConfigParser` utility (`src/utils/`) wraps the OCIO C++ API to enumerate displays/views/colorspaces for the UI.
**UI panels** (all `src/ui/`):
- `StageEditorPanel` — edit target, dirty state, drag-drop sublayer ordering.
- `SceneHierarchyPanel` — prim tree with type icons.
- `PropertyPanel` — attribute inspector/editor.
- `TimelinePanel` — transport controls, loop/bounce, frame scrub.
- `CurveEditorPanel` — Maya-style F-curve editor with Bezier round-trip to USD time samples.
**GL loader:** `glad` (generated loader in `src/utils/GLExt.h`). Do not call GL functions before `gladLoadGL`.
**Logging:** `LOG_INFO / LOG_WARNING / LOG_ERROR` macros (`src/utils/Logger.h`). USD diagnostics (`TF_WARN`, `TF_ERROR`) are routed to the same logger via a `TfDiagnosticMgr::Delegate` installed in `UsdSceneRenderer::InitRenderer()`.
**DLL layout (Windows):**
- Exe + all USD/OCIO/FFmpeg DLLs live in `build/Release/`.
- Hydra plugin DLLs live in `build/Release/usd/` (so `plugInfo.json`'s `LibraryPath "../<name>.dll"` resolves correctly).
---
# 12-rule template
These rules apply to every task in this project unless explicitly overridden. These rules apply to every task in this project unless explicitly overridden.
Bias: caution over speed on non-trivial work. Use judgment on trivial tasks. Bias: caution over speed on non-trivial work. Use judgment on trivial tasks.
+603 -11
View File
@@ -13,11 +13,181 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(OpenUSD REQUIRED) find_package(OpenUSD REQUIRED)
find_package(Imgui REQUIRED) find_package(Imgui REQUIRED)
find_package(ImguiNodeEditor REQUIRED)
find_package(Glad REQUIRED) find_package(Glad REQUIRED)
find_package(FFmpeg REQUIRED)
# OpenColorIO — provided by the OpenUSD distribution (USD built with OCIO support)
set(OpenColorIO_DIR
"${OpenUSD_ROOT_DIR}/lib/cmake/OpenColorIO"
CACHE PATH "OpenColorIO CMake config dir" FORCE)
find_package(OpenColorIO CONFIG REQUIRED)
# USD build that includes hdEmbree (built with PXR_ENABLE_EMBREE_PLUGIN=ON).
# Can be a separate install from OpenUSD_ROOT_DIR; leave empty to search there instead.
set(HDEMBREE_USD_ROOT "" CACHE PATH "USD build containing hdEmbree plugin (separate from OpenUSD_ROOT_DIR)")
# Only needed if embree*.dll are not inside HDEMBREE_USD_ROOT/bin.
set(EMBREE_LOCATION "" CACHE PATH "Embree SDK bin/ directory (leave empty if bundled in HDEMBREE_USD_ROOT/bin)")
# ── hdEmbree detection ───────────────────────────────────────────────────────
if(HDEMBREE_USD_ROOT AND EXISTS "${HDEMBREE_USD_ROOT}")
set(_hdembree_root "${HDEMBREE_USD_ROOT}")
else()
set(_hdembree_root "${OpenUSD_ROOT_DIR}")
endif()
# hdEmbree.dll lives at plugin/usd/hdEmbree.dll; the subdir holds only plugin metadata.
set(HDEMBREE_PLUGIN_DIR "${_hdembree_root}/plugin/usd/hdEmbree")
set(HDEMBREE_DLL "${_hdembree_root}/plugin/usd/hdEmbree.dll")
if(EXISTS "${HDEMBREE_PLUGIN_DIR}")
set(OPENUSD_HAS_EMBREE TRUE)
if(EMBREE_LOCATION AND EXISTS "${EMBREE_LOCATION}")
set(_embree_dll_dir "${EMBREE_LOCATION}")
else()
set(_embree_dll_dir "${_hdembree_root}/bin")
endif()
# Skip tbb*.dll — already supplied by the main USD install to avoid conflicts.
file(GLOB EMBREE_DLLS "${_embree_dll_dir}/embree*.dll")
else()
set(OPENUSD_HAS_EMBREE FALSE)
message(STATUS "hdEmbree: NOT found — set HDEMBREE_USD_ROOT to a USD build with PXR_ENABLE_EMBREE_PLUGIN=ON")
endif()
# ── hdArnold detection ───────────────────────────────────────────────────────
# HDARNOLD_ROOT: CMAKE_INSTALL_PREFIX used when building arnold-usd
set(HDARNOLD_ROOT "" CACHE PATH "arnold-usd install directory (the one containing plugin/hdArnold.dll)")
# ARNOLD_LOCATION: Arnold SDK root (MtoA dir) — needed to deploy ai.dll
set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/2026) for ai.dll deployment")
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(OPENUSD_HAS_ARNOLD TRUE)
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
# arnold-usd renamed the ndr plugin to node_registry (USD merged ndr into
# sdr as of PXR_VERSION 2505) and added a usdImaging adapter plugin.
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold.dll")
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold")
set(USDIMAGINGARNOLD_DLL "${HDARNOLD_ROOT}/plugin/usdImagingArnold.dll")
set(USDIMAGINGARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/usdImagingArnold")
if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
# ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
file(GLOB ARNOLD_RUNTIME_DLLS
"${ARNOLD_LOCATION}/bin/ai*.dll"
"${ARNOLD_LOCATION}/bin/AdClm*.dll"
"${ARNOLD_LOCATION}/bin/AdskLicensing*.dll"
"${ARNOLD_LOCATION}/bin/cer.dll"
)
else()
set(ARNOLD_AI_DLL "")
set(ARNOLD_RUNTIME_DLLS "")
endif()
else()
set(OPENUSD_HAS_ARNOLD FALSE)
message(STATUS "hdArnold: NOT found — set HDARNOLD_ROOT to the arnold-usd install dir and ARNOLD_LOCATION to the MtoA root")
endif()
# ── Cycles hdCycles Hydra render delegate ────────────────────────────────────
# Build Blender Cycles from third_party/cycles using ExternalProject.
# Set WITH_CYCLES=ON to enable; requires MSVC, and uses the precompiled libs
# bundled in third_party/cycles/lib/windows_x64/.
# hdCycles.dll is deployed to usd/ alongside hdEmbree and hdArnold.
option(WITH_CYCLES "Build Cycles Hydra render delegate from third_party/cycles" OFF)
set(CYCLES_BINARY_DIR "${CMAKE_BINARY_DIR}/cycles_build")
set(CYCLES_INSTALL_DIR "${CMAKE_BINARY_DIR}/cycles_install")
set(HDCYCLES_DLL "${CYCLES_INSTALL_DIR}/hydra/hdCycles.dll")
set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
# not in the install output — deploy it explicitly.
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")
# OpenColorIO_2_5.dll: Cycles' bundled prebuilt OpenImageIO.dll was compiled
# against Cycles' own OCIO 2.5 (forced via -DOpenColorIO_DIR below, to avoid
# USD's OCIO headers shadowing Cycles' during the ExternalProject build) —
# distinct from and required alongside USD's own OpenColorIO_2_2.dll. It also
# lives only in the precompiled lib tree, not in cycles_install's output.
set(CYCLES_OCIO25_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/bin/OpenColorIO_2_5.dll")
if(WITH_CYCLES)
include(ExternalProject)
# Resolve absolute USD root so we can forward it to the sub-build.
# FindOpenUSD.cmake stores the path in OpenUSD_ROOT_DIR.
if(NOT OpenUSD_ROOT_DIR)
message(FATAL_ERROR "WITH_CYCLES requires OpenUSD_ROOT_DIR to be set (run find_package(OpenUSD) first).")
endif()
ExternalProject_Add(CyclesBuild
SOURCE_DIR "${CMAKE_SOURCE_DIR}/third_party/cycles"
BINARY_DIR "${CYCLES_BINARY_DIR}"
INSTALL_DIR "${CYCLES_INSTALL_DIR}"
CMAKE_ARGS
# Always Release — Cycles' Debug build is slow and rarely needed here.
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
# Point Cycles at our app's USD so hdCycles is ABI-compatible.
-DPXR_ROOT=${OpenUSD_ROOT_DIR}
# Hydra delegate is the only deliverable we need.
-DWITH_CYCLES_HYDRA_RENDER_DELEGATE=ON
-DWITH_CYCLES_USD=ON
# Use bundled precompiled libs from lib/windows_x64/.
-DWITH_LIBS_PRECOMPILED=ON
# Turn off heavy optional features that aren't needed for the delegate.
-DWITH_CYCLES_STANDALONE_GUI=OFF
-DWITH_CYCLES_DEVICE_CUDA=OFF
-DWITH_CYCLES_DEVICE_OPTIX=OFF
-DWITH_CYCLES_DEVICE_HIP=OFF
-DWITH_CYCLES_DEVICE_ONEAPI=OFF
-DWITH_CYCLES_LOGGING=OFF
# USD ships OpenVDB v9; Cycles prebuilt is v13 — ABI mismatch, both named
# openvdb.dll. Disable openvdb in Cycles to avoid the version conflict.
-DWITH_CYCLES_OPENVDB=OFF
# Embree4 links tbb12.dll; USD already loads tbb.dll at process startup.
# oneTBB detects the older tbb.dll and aborts initialization (DLL_INIT_FAILED).
# Disable Embree so Cycles uses its own BVH traversal instead.
-DWITH_CYCLES_EMBREE=OFF
# ---- pxrConfig.cmake dependency overrides ----
# pxrConfig.cmake runs find_dependency() for these before Cycles'
# precompiled lib paths are on CMAKE_PREFIX_PATH, so we must supply
# them explicitly. The Python3 vars also override the hardcoded
# CI build-machine path embedded in pxrConfig.cmake.
-DPython3_EXECUTABLE=$ENV{LOCALAPPDATA}/Programs/Python/Python312/python.exe
-DPython3_LIBRARY=$ENV{LOCALAPPDATA}/Programs/Python/Python312/libs/python312.lib
-DPython3_INCLUDE_DIR=$ENV{LOCALAPPDATA}/Programs/Python/Python312/include
-DTBB_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/lib/cmake/TBB
-DMaterialX_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/MaterialX
# OpenSubdiv and Imath cmake configs are bundled in the USD install.
# We must supply their dirs so pxrTargets.cmake can resolve the imported targets.
-DOpenSubdiv_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/OpenSubdiv
-DImath_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/Imath
# Force Cycles to use its own prebuilt OCIO 2.5, not the OCIO 2.1
# bundled in the new USD include dir. Without this, the new USD's
# include/OpenColorIO/ (OCIO 2.1) shadows Cycles' prebuilt OCIO 2.5
# headers and the colorspace.cpp API calls fail to compile.
-DOpenColorIO_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/lib/cmake/OpenColorIO
BUILD_COMMAND
${CMAKE_COMMAND} --build <BINARY_DIR> --config Release
INSTALL_COMMAND
${CMAKE_COMMAND} --install <BINARY_DIR> --config Release
BUILD_BYPRODUCTS
"${HDCYCLES_DLL}"
)
set(OPENUSD_HAS_CYCLES TRUE)
else()
set(OPENUSD_HAS_CYCLES FALSE)
message(STATUS "hdCycles: disabled — set WITH_CYCLES=ON to build from third_party/cycles")
endif()
# Collect source files # Collect source files
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp") file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp") file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp")
# src/ui/NodeEditor is the in-tree vendored imgui-node-editor subset — already
# compiled into the imgui_node_editor_impl target by FindImguiNodeEditor.cmake
# (with its own required include dirs/defines); excluded here so it isn't
# compiled a second time, incorrectly, as part of this target's own sources.
list(FILTER UI_SOURCES EXCLUDE REGEX "/src/ui/NodeEditor/")
file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp") file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp")
set(MAIN_SOURCE "src/main.cpp") set(MAIN_SOURCE "src/main.cpp")
@@ -41,9 +211,24 @@ target_include_directories(UsdLayerManager PRIVATE
target_link_libraries(UsdLayerManager PRIVATE target_link_libraries(UsdLayerManager PRIVATE
OpenUSD::OpenUSD OpenUSD::OpenUSD
Imgui::Imgui Imgui::Imgui
ImguiNodeEditor::ImguiNodeEditor
Glad::Glad Glad::Glad
FFmpeg::FFmpeg
OpenColorIO::OpenColorIO
ole32
shell32
) )
target_include_directories(UsdLayerManager PRIVATE
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
)
# Cycles is an ExternalProject; UsdLayerManager must wait for it to finish
# before POST_BUILD copies hdCycles.dll to the output directory.
if(WITH_CYCLES)
add_dependencies(UsdLayerManager CyclesBuild)
endif()
# Platform-specific settings # Platform-specific settings
if(WIN32) if(WIN32)
# Windows-specific settings # Windows-specific settings
@@ -53,6 +238,52 @@ if(WIN32)
_CRT_SECURE_NO_WARNINGS _CRT_SECURE_NO_WARNINGS
) )
# OpenColorIO DLL — copy from USD bin/. Filename is version-suffixed
# (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
# OCIO version changes, so glob for it instead of hardcoding the version.
file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
if(OPENCOLORIO_RUNTIME_DLL)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${OPENCOLORIO_RUNTIME_DLL}
"$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying OpenColorIO runtime DLL..."
)
else()
message(WARNING "OpenColorIO runtime DLL not found in ${OpenUSD_ROOT_DIR}/bin")
endif()
# FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
if(FFMPEG_RUNTIME_DLLS)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${FFMPEG_RUNTIME_DLLS}
"$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying FFmpeg runtime DLLs..."
)
endif()
# cmake -P script to copy USD bin/ DLLs excluding CRT DLLs (msvcp, vcruntime, etc.).
# The USD distribution bundles older CRT DLLs (14.34) that conflict with Cycles
# binaries built against the newer system CRT (14.42+). The system CRT is
# always present on Windows 10+ — no need to bundle our own copy.
file(WRITE "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
"file(GLOB _all_dlls \"${OpenUSD_ROOT_DIR}/bin/*.dll\")\n"
"set(_dlls)\n"
"foreach(_dll \${_all_dlls})\n"
" get_filename_component(_name \"\${_dll}\" NAME)\n"
" string(TOLOWER \"\${_name}\" _lname)\n"
" if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
" continue()\n"
" endif()\n"
" list(APPEND _dlls \"\${_dll}\")\n"
"endforeach()\n"
"if(_dlls)\n"
" file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
"endif()\n"
)
# Copy runtime DLLs to output directory for each configuration # Copy runtime DLLs to output directory for each configuration
add_custom_command(TARGET UsdLayerManager POST_BUILD add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>" COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>"
@@ -60,10 +291,10 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_BIN_DIR}" "${OpenUSD_BIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>" "$<TARGET_FILE_DIR:UsdLayerManager>"
# OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. # OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. (CRT DLLs excluded)
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND}
"${OpenUSD_ROOT_DIR}/bin" "-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
"$<TARGET_FILE_DIR:UsdLayerManager>" -P "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_ROOT_DIR}/lib/usd" "${OpenUSD_ROOT_DIR}/lib/usd"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd" "$<TARGET_FILE_DIR:UsdLayerManager>/usd"
@@ -78,6 +309,120 @@ if(WIN32)
"$<TARGET_FILE_DIR:UsdLayerManager>" "$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying runtime DLLs and USD plugins..." COMMENT "Copying runtime DLLs and USD plugins..."
) )
if(OPENUSD_HAS_EMBREE)
add_custom_command(TARGET UsdLayerManager POST_BUILD
# plugInfo.json has LibraryPath "../hdEmbree.dll" relative to usd/hdEmbree/,
# which resolves to usd/hdEmbree.dll — so the DLL must live in usd/, not exe root.
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${HDEMBREE_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDEMBREE_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree"
COMMENT "Copying hdEmbree plugin..."
)
if(EMBREE_DLLS)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${EMBREE_DLLS}
"$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying Embree runtime DLLs..."
)
endif()
endif()
if(OPENUSD_HAS_ARNOLD)
add_custom_command(TARGET UsdLayerManager POST_BUILD
# DLLs go in usd/ alongside their metadata dirs (LibraryPath "../hdArnold.dll")
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${HDARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NDRARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${NDRARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${USDIMAGINGARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${USDIMAGINGARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold"
COMMENT "Copying hdArnold plugin..."
)
if(ARNOLD_RUNTIME_DLLS)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ARNOLD_RUNTIME_DLLS}
"$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying Arnold runtime DLLs (ai, OIDN, licensing)..."
)
endif()
endif()
if(OPENUSD_HAS_CYCLES)
# hdCycles uses the same usd/ layout as hdEmbree:
# usd/hdCycles.dll + usd/hdCycles/resources/plugInfo.json
# The bundled plugInfo.json has Root=".." and LibraryPath="../hdCycles.dll"
# which resolves correctly once the plugin dir lives in usd/.
#
# Runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, vulkan, etc.)
# live in CYCLES_INSTALL_DIR root and must be next to the exe. We can't
# file(GLOB) them at configure time (ExternalProject hasn't run yet), so we
# write a small cmake -P script that globs them at build time instead.
# The script runs at build time (after ExternalProject finishes) so it can glob
# the actual install output. CRT/Windows DLLs are excluded — they must come
# from the system or the main exe's toolchain, not from Cycles' bundled copy.
file(WRITE "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
"file(GLOB _all_dlls \"${CYCLES_INSTALL_DIR}/*.dll\")\n"
"set(_dlls)\n"
"foreach(_dll \${_all_dlls})\n"
" get_filename_component(_name \"\${_dll}\" NAME)\n"
" string(TOLOWER \"\${_name}\" _lname)\n"
# Exclude CRT DLLs (system must supply these; Cycles bundles an older version).
" if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
" continue()\n"
" endif()\n"
# Exclude debug-variant DLLs (not needed in release, just wasted space).
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
# openjph.0.25d.dll → version digit directly followed by "d.dll"
# NOTE: must require a digit before the trailing "d" — a bare "d[.]dll$"
# also matches legitimate Release DLLs like IlmThread.dll (ends in "...ead.dll").
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"[0-9]d[.]dll$\")\n"
" continue()\n"
" endif()\n"
" list(APPEND _dlls \"\${_dll}\")\n"
"endforeach()\n"
"if(_dlls)\n"
" file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
"endif()\n"
)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${HDCYCLES_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDCYCLES_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles"
COMMAND ${CMAKE_COMMAND}
"-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
-P "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
# tbb12.dll is required by openvdb/embree4 but is not in the install output
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_TBB12_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
# OpenColorIO_2_5.dll is required by Cycles' bundled OpenImageIO.dll
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_OCIO25_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/OpenColorIO_2_5.dll"
COMMENT "Copying hdCycles plugin and runtime DLLs..."
)
endif()
endif() endif()
# Compiler warnings # Compiler warnings
@@ -100,33 +445,213 @@ install(DIRECTORY ${OpenUSD_BIN_DIR}/
) )
# Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR, # Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR,
# OpenImageIO, zlib, etc. These are separate from lib/ and also required at runtime. # OpenImageIO, zlib, etc. Exclude CRT DLLs: the system VC++ Redistributable supplies
# these at runtime; bundling an older copy (14.34) causes crashes on machines with
# Cycles or other DLLs compiled against the newer CRT (14.42+).
install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/ install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/
DESTINATION bin DESTINATION bin
FILES_MATCHING PATTERN "*.dll" FILES_MATCHING PATTERN "*.dll"
PATTERN "msvcp*" EXCLUDE
PATTERN "vcruntime*" EXCLUDE
PATTERN "ucrtbase*" EXCLUDE
PATTERN "concrt*" EXCLUDE
PATTERN "api-ms-win-*" EXCLUDE
) )
install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd
DESTINATION bin DESTINATION bin
) )
install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd/
DESTINATION bin/usd DESTINATION bin/usd
) )
install(FILES ${OpenUSD_ROOT_DIR}/plugin/usd/hdStorm.dll if(OPENUSD_HAS_EMBREE)
# Install to bin/usd/ so plugInfo.json's LibraryPath "../hdEmbree.dll" resolves correctly.
install(FILES "${HDEMBREE_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${HDEMBREE_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
if(EMBREE_DLLS)
install(FILES ${EMBREE_DLLS}
DESTINATION bin DESTINATION bin
) OPTIONAL
)
endif()
endif()
# Install Python runtime DLLs required by OpenUSD. if(OPENUSD_HAS_ARNOLD)
install(FILES install(FILES "${HDARNOLD_DLL}" "${NDRARNOLD_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${HDARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${NDRARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
install(FILES "${USDIMAGINGARNOLD_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
if(ARNOLD_RUNTIME_DLLS)
install(FILES ${ARNOLD_RUNTIME_DLLS}
DESTINATION bin
OPTIONAL
)
endif()
endif()
if(OPENUSD_HAS_CYCLES)
install(FILES "${HDCYCLES_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${HDCYCLES_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
# Cycles runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, etc.).
# No OPTIONAL: install(DIRECTORY) disallows it with FILES_MATCHING, and the
# dir always exists here since it is produced by the CyclesBuild step.
#
# EXCLUDE the MSVC CRT and api-ms-win DLLs that Cycles bundles: they are
# older than what the system ships (v14.34 vs system v14.42+), and placing
# an older MSVCP140.dll/VCRUNTIME140.dll ahead of System32 on the DLL
# search path causes sycl8.dll (built with newer CRT) to fail DllMain with
# ERROR_DLL_INIT_FAILED (1114) when hdCycles.dll is loaded.
# The system VC++ Redistributable always satisfies these deps at runtime.
install(DIRECTORY "${CYCLES_INSTALL_DIR}/"
DESTINATION bin
FILES_MATCHING
PATTERN "*.dll"
PATTERN "MSVCP140*.dll" EXCLUDE
PATTERN "VCRUNTIME140*.dll" EXCLUDE
PATTERN "ucrtbase*.dll" EXCLUDE
PATTERN "api-ms-win-*.dll" EXCLUDE
PATTERN "OpenColorIO_d_*.dll" EXCLUDE # debug OCIO build
PATTERN "openjph.*d.dll" EXCLUDE # debug OpenJPH
)
# tbb12.dll (used by openvdb/embree4) lives in the precompiled lib tree, not install output
install(FILES "${CYCLES_TBB12_DLL}"
DESTINATION bin
OPTIONAL
)
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
install(FILES "${CYCLES_OCIO25_DLL}"
DESTINATION bin
OPTIONAL
)
endif()
# Install Python runtime DLLs required by OpenUSD and Cycles.
# python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
# (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
set(_python_runtime_dlls
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll" "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll" "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
)
install(FILES ${_python_runtime_dlls}
DESTINATION bin DESTINATION bin
OPTIONAL # OPTIONAL so the install doesn't fail on machines with a OPTIONAL # OPTIONAL so the install doesn't fail on machines with a
# system-wide Python; in that case the DLL is on PATH already. # system-wide Python; in that case the DLL is on PATH already.
) )
# ---------------------------------------------------------------------------
# OCIO: download ACES 1.2 config at configure time if not present
# ---------------------------------------------------------------------------
set(_ocio_config_dir "${CMAKE_SOURCE_DIR}/resources/OpenColorIO-Configs/aces_1.2")
if(NOT EXISTS "${_ocio_config_dir}/config.ocio")
message(STATUS "Downloading ACES 1.2 OCIO config (~124 MB) ...")
set(_ocio_zip "${CMAKE_BINARY_DIR}/aces_1.2.zip")
file(DOWNLOAD
"https://github.com/colour-science/OpenColorIO-Configs/releases/download/v1.2/OpenColorIO-Config-ACES-1.2.zip"
"${_ocio_zip}"
SHOW_PROGRESS
STATUS _ocio_dl_status
)
list(GET _ocio_dl_status 0 _ocio_dl_code)
if(_ocio_dl_code EQUAL 0)
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xf "${_ocio_zip}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
)
file(COPY "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2/aces_1.2/"
DESTINATION "${_ocio_config_dir}")
file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2")
file(REMOVE "${_ocio_zip}")
message(STATUS "ACES 1.2 OCIO config installed to ${_ocio_config_dir}")
else()
message(WARNING "ACES 1.2 OCIO download failed (${_ocio_dl_status}) — OCIO will not be bundled.")
endif()
endif()
# ---------------------------------------------------------------------------
# HDRI: shader-ball lighting presets (Poly Haven, CC0) at configure time
# ---------------------------------------------------------------------------
set(_hdri_dir "${CMAKE_SOURCE_DIR}/resources/hdri")
set(_hdri_files
kloofendal_48d_partly_cloudy_puresky_1k.exr
lebombo_1k.exr
artist_workshop_1k.exr
venice_sunset_1k.exr
)
file(MAKE_DIRECTORY "${_hdri_dir}")
foreach(_hdri ${_hdri_files})
if(NOT EXISTS "${_hdri_dir}/${_hdri}")
message(STATUS "Downloading HDRI ${_hdri} ...")
file(DOWNLOAD
"https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/${_hdri}"
"${_hdri_dir}/${_hdri}"
STATUS _hdri_dl_status
)
list(GET _hdri_dl_status 0 _hdri_dl_code)
if(NOT _hdri_dl_code EQUAL 0)
file(REMOVE "${_hdri_dir}/${_hdri}")
message(WARNING "HDRI download failed for ${_hdri} (${_hdri_dl_status}) — the shader-ball lighting preset will fall back to a distant light.")
endif()
endif()
endforeach()
# ---------------------------------------------------------------------------
# VDB: cloud volume for the shader-ball preview (OpenVDB sample model) at
# configure time if not present
# ---------------------------------------------------------------------------
set(_vdb_dir "${CMAKE_SOURCE_DIR}/resources/vdb")
file(MAKE_DIRECTORY "${_vdb_dir}")
if(NOT EXISTS "${_vdb_dir}/bunny_cloud.vdb")
message(STATUS "Downloading OpenVDB sample bunny_cloud.vdb ...")
set(_vdb_zip "${CMAKE_BINARY_DIR}/bunny_cloud.zip")
file(DOWNLOAD
"https://artifacts.aswf.io/io/aswf/openvdb/models/bunny_cloud.vdb/1.0.0/bunny_cloud.vdb-1.0.0.zip"
"${_vdb_zip}"
STATUS _vdb_dl_status
)
list(GET _vdb_dl_status 0 _vdb_dl_code)
if(_vdb_dl_code EQUAL 0)
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xf "${_vdb_zip}"
WORKING_DIRECTORY "${_vdb_dir}"
)
file(REMOVE "${_vdb_zip}")
message(STATUS "bunny_cloud.vdb installed to ${_vdb_dir}")
else()
message(WARNING "OpenVDB sample download failed (${_vdb_dl_status}) — the Cloud Volume preview shape will be unavailable.")
endif()
endif()
# Copy resources to build directory # Copy resources to build directory
file(COPY ${CMAKE_SOURCE_DIR}/resources file(COPY ${CMAKE_SOURCE_DIR}/resources
DESTINATION ${CMAKE_BINARY_DIR} DESTINATION ${CMAKE_BINARY_DIR}
@@ -142,7 +667,42 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/icons" "${CMAKE_SOURCE_DIR}/resources/icons"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons" "$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
COMMENT "Copying fonts and SVG icons to build output" COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/hdri"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/preview"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/preview"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/vdb"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
# MaterialX standard library (nodedefs + genosl headers such as mx_funcs.h).
# hdArnold reads PXR_MTLX_STDLIB_SEARCH_PATHS to locate these; without them
# Arnold's OSL compile of MaterialX shaders fails with
# "fatal error: 'mx_funcs.h' file not found". Application.cpp points the
# env var at this deployed copy.
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_ROOT_DIR}/libraries"
"$<TARGET_FILE_DIR:UsdLayerManager>/libraries"
COMMENT "Copying fonts, SVG icons, HDRI presets, preview shapes and MaterialX stdlib to build output"
)
# Copy ACES OCIO config to build output — only on first build (skipped if already present).
if(EXISTS "${_ocio_config_dir}/config.ocio")
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND}
"-DSRC=${_ocio_config_dir}"
"-DDST=$<TARGET_FILE_DIR:UsdLayerManager>/resources/OpenColorIO-Configs/aces_1.2"
-P "${CMAKE_SOURCE_DIR}/cmake/CopyDirIfMissing.cmake"
COMMENT "Copying ACES OCIO config to build output (first time only)"
)
endif()
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
install(FILES
${OPENCOLORIO_RUNTIME_DLL}
DESTINATION bin
OPTIONAL
) )
# Install resources (icons, fonts placeholder) next to the exe so the # Install resources (icons, fonts placeholder) next to the exe so the
@@ -151,6 +711,14 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
DESTINATION bin DESTINATION bin
) )
# MaterialX standard library — required by hdArnold's OSL codegen (mx_funcs.h).
# See the matching POST_BUILD copy above and PXR_MTLX_STDLIB_SEARCH_PATHS in
# Application.cpp.
install(DIRECTORY "${OpenUSD_ROOT_DIR}/libraries"
DESTINATION bin
OPTIONAL
)
# Install Inter font next to the exe under resources/font/ # Install Inter font next to the exe under resources/font/
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc". # ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
@@ -168,6 +736,26 @@ message(STATUS "ImGui found: ${Imgui_FOUND}")
message(STATUS "OpenGL found: ${OPENGL_FOUND}") message(STATUS "OpenGL found: ${OPENGL_FOUND}")
message(STATUS "Glad found: ${Glad_FOUND}") message(STATUS "Glad found: ${Glad_FOUND}")
message(STATUS "Build tests: ${BUILD_TESTS}") message(STATUS "Build tests: ${BUILD_TESTS}")
message(STATUS "hdEmbree support: ${OPENUSD_HAS_EMBREE}")
if(OPENUSD_HAS_EMBREE AND EMBREE_DLLS)
message(STATUS "Embree DLLs: ${EMBREE_DLLS}")
elseif(OPENUSD_HAS_EMBREE)
message(WARNING "hdEmbree found but no Embree DLLs detected — set EMBREE_LOCATION to the Embree SDK bin/ dir or check HDEMBREE_USD_ROOT/bin")
endif()
message(STATUS "hdArnold support: ${OPENUSD_HAS_ARNOLD}")
if(OPENUSD_HAS_ARNOLD)
message(STATUS "hdArnold DLL: ${HDARNOLD_DLL}")
message(STATUS "Arnold runtime DLLs: ${ARNOLD_RUNTIME_DLLS}")
if(NOT ARNOLD_AI_DLL)
message(WARNING "hdArnold found but ai.dll not located — set ARNOLD_LOCATION to the MtoA root (e.g. C:/Autodesk/mtoa/5.5.0/2026)")
endif()
endif()
message(STATUS "hdCycles support: ${OPENUSD_HAS_CYCLES}")
if(OPENUSD_HAS_CYCLES)
message(STATUS "Cycles source: ${CMAKE_SOURCE_DIR}/third_party/cycles")
message(STATUS "Cycles build: ${CYCLES_BINARY_DIR}")
message(STATUS "Cycles install: ${CYCLES_INSTALL_DIR}")
endif()
message(STATUS "=======================================") message(STATUS "=======================================")
message(STATUS "") message(STATUS "")
@@ -186,11 +774,13 @@ add_executable(ViewportDisplayTest
target_include_directories(ViewportDisplayTest PRIVATE target_include_directories(ViewportDisplayTest PRIVATE
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
) )
target_link_libraries(ViewportDisplayTest PRIVATE target_link_libraries(ViewportDisplayTest PRIVATE
OpenUSD::OpenUSD OpenUSD::OpenUSD
Glad::Glad Glad::Glad
FFmpeg::FFmpeg
) )
if(WIN32) if(WIN32)
@@ -232,11 +822,13 @@ add_executable(RendererDiagnosticTest
target_include_directories(RendererDiagnosticTest PRIVATE target_include_directories(RendererDiagnosticTest PRIVATE
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include
"${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
) )
target_link_libraries(RendererDiagnosticTest PRIVATE target_link_libraries(RendererDiagnosticTest PRIVATE
OpenUSD::OpenUSD OpenUSD::OpenUSD
Glad::Glad Glad::Glad
FFmpeg::FFmpeg
) )
if(WIN32) if(WIN32)
+8 -4
View File
@@ -9,11 +9,14 @@
"binaryDir": "${sourceDir}/build", "binaryDir": "${sourceDir}/build",
"cacheVariables": { "cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install", "CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD_v25.05", "CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.11",
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7", "IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
"CMAKE_CXX_STANDARD": "17", "CMAKE_CXX_STANDARD": "17",
"CMAKE_CXX_STANDARD_REQUIRED": "ON", "CMAKE_CXX_STANDARD_REQUIRED": "ON",
"BUILD_TESTS": "ON" "BUILD_TESTS": "OFF",
"WITH_CYCLES": "ON",
"HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
} }
}, },
{ {
@@ -32,7 +35,7 @@
"description": "Optimized release build", "description": "Optimized release build",
"inherits": "default", "inherits": "default",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Release", "CMAKE_BUILD_TYPE": "RelWithDebInfo",
"BUILD_TESTS": "OFF" "BUILD_TESTS": "OFF"
} }
}, },
@@ -64,7 +67,8 @@
"name": "release", "name": "release",
"displayName": "Release Build", "displayName": "Release Build",
"configurePreset": "release", "configurePreset": "release",
"configuration": "Release" "configuration": "Release",
"targets": ["install"]
}, },
{ {
"name": "no-tests", "name": "no-tests",
+56 -20
View File
@@ -1,30 +1,46 @@
# USD Layer Manager # USD Layer Manager
A C++17 / Windows desktop application providing a **Maya Render Layers-style** USD scene editor using **OpenUSD v25.05**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, and interact with 3D scenes through a Hydra viewport. A C++17 / Windows desktop application providing a **Maya-style** USD scene and look-dev editor using **OpenUSD v25.11**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, author UsdShade material graphs with a live shader-ball preview, animate with a Bezier curve editor, and interact with 3D scenes through a multi-viewport Hydra renderer with OCIO color management.
![Screenshot](docs/icons/screenshot_01.png) ![Screenshot](docs/icons/screenshot_01.png)
## Features ## Features
- **Layer Stack Management** — Create, reorder, mute/unmute sublayers with full undo support - **Layer Stack Management** — `StageEditorPanel`: create, reorder, mute/unmute sublayers, set the edit target, add existing files as sublayers, with full undo support
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename prims; add/replace references - **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename, reparent, and group prims; add/replace references
- **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms - **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates; grid overlay; selection bounding boxes; camera wireframes - **Material Editor** — Hypershade-style node-graph editor for UsdShade/MaterialX networks (`MaterialEditorPanel`): material browser, drag-to-connect node canvas, per-node/per-pin display modes, Sdr-driven node creation menu and enum dropdowns, colorSpace authoring on texture inputs, and a live shader-ball preview (`MaterialPreviewRenderer`) with its own renderer/HDRI/preview-shape controls. In-canvas node thumbnails (`NodeThumbnailCache`) show texture previews and per-node shader-ball renders, amortized across frames and re-rendered until progressive delegates (Arnold/Cycles/Embree) fully converge
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera and render settings; Space to maximize/restore - **Timeline & Curve Editor** — Transport controls with loop/bounce and Auto-Key (`TimelinePanel`); a Maya-style F-curve editor (`CurveEditorPanel`) with Bezier round-trip to USD time samples; Playblast export to MP4 via `MovieEncoder` (libavcodec)
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); OCIO-based color management (sRGB or full display/view/look pipeline) applied as a post-Hydra GL pass; grid overlay; selection bounding boxes; camera/light wireframes
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera, render delegate, and AOV; Space to maximize/restore
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages - **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages
- **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space - **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space
- **Undo/Redo** — Full command history for prim creation/deletion, attribute edits, transform changes, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration - **Undo/Redo** — Full command history (`ICommand` subclasses under `src/core/commands/`) covering prim creation/deletion/reparenting/grouping, attribute edits, transform changes, shader-node creation/connection, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing - **Multi-select** — Rectangular selection in viewport with cross-panel syncing
| Material Editor (Arnold) | Curve Editor | Quad Viewport (4 delegates) | MaterialX import |
|---|---|---|---|
| ![Material Editor](docs/icons/screenshot_06.png) | ![Curve Editor](docs/icons/screenshot_02.png) | ![Quad Viewport](docs/icons/screenshot_03.png) | ![MaterialX](docs/icons/screenshot_04.png) |
## Dependencies ## Dependencies
| Dependency | Version | Notes | | Dependency | Version | Notes |
|---|---|---| |---|---|---|
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.05 | Prebuilt; found via `FindOpenUSD.cmake` | | [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.11 | Prebuilt; found via `FindOpenUSD.cmake` |
| [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends | | [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends |
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader | | [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) | | Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
### Optional Render Delegates
| Delegate | Renderer Version | Variable | Notes |
|---|---|---|---|
| **hdEmbree** | Embree 4.3.3 | `HDEMBREE_USD_ROOT` (optional) | Bundled directly in `third_party/OpenUSD-v25.11` (built with `--embree`); set `HDEMBREE_USD_ROOT` only if using a separate USD build, and `EMBREE_LOCATION` if Embree DLLs aren't in its `bin/` |
| **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) (tag `Arnold-7.4.5.1`, for USD 25.11's `ndr``sdr` API change) install dir + MtoA root for `ai.dll` |
| **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC |
All delegate paths are pre-configured in `CMakePresets.json`.
## Build ## Build
### Prerequisites ### Prerequisites
@@ -41,16 +57,19 @@ cmake --build build --config Release
cmake --install build --config Release cmake --install build --config Release
``` ```
The installed `App.exe` lives in `install/bin/`. The installed executable lives in `install/bin/`.
**Presets**`default` (Release + tests), `debug`, `release`, `no-tests`. **Presets**`default` (`Release`, `WITH_CYCLES=ON`, all render delegates), `debug` (`Debug`), `release` (`RelWithDebInfo`, installs), `no-tests` (`Release`). All presets currently build with `BUILD_TESTS=OFF`.
### Building with Cycles
`WITH_CYCLES=ON` is set in the default preset. The first build compiles Blender Cycles from `third_party/cycles` via ExternalProject — expect a significantly longer initial build. Subsequent builds are incremental.
Cycles runtime dependencies (`embree4.dll`, `openvdb.dll`, `OpenImageDenoise.dll`, `tbb12.dll`, etc.) are automatically deployed to the output directory by a post-build step. CRT DLLs bundled inside the Cycles install are excluded to avoid version conflicts with the app's own MSVC runtime.
### Tests ### Tests
```powershell There is currently no working test runner: `BUILD_TESTS` is `OFF` in every preset. The two test executables (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths and don't build — fix those before turning `BUILD_TESTS` on.
cmake --build build --config Release
ctest --test-dir build -C Release
```
## Architecture ## Architecture
@@ -61,16 +80,23 @@ src/
│ ├── UsdStageManager — Stage open/create/save/close lifecycle │ ├── UsdStageManager — Stage open/create/save/close lifecycle
│ ├── LayerManager — Layer stack introspection and mutation │ ├── LayerManager — Layer stack introspection and mutation
│ ├── PropertyManager — Property read/write via edit target with undo │ ├── PropertyManager — Property read/write via edit target with undo
│ ├── MaterialManager — UsdShade graph introspection + Sdr node-type/enum discovery
│ ├── CommandHistory — Undo/redo stack for ICommand instances │ ├── CommandHistory — Undo/redo stack for ICommand instances
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays │ ├── UsdSceneRenderer — Hydra rendering + picking + overlays + OCIO color correction
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera │ ├── ViewportCamera — Free / USD-camera-driven orbital camera
│ └── commands/ — ICommand subclasses for all mutable operations │ └── commands/ — ICommand subclasses for all mutable operations
│ ├── TransformCommand — Undoable translate/rotate/scale │ ├── TransformCommand — Undoable translate/rotate/scale
│ ├── CreatePrimCommand — Undoable prim creation (with camera orientation) │ ├── CreatePrimCommand — Undoable prim creation (with camera orientation)
│ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot) │ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot)
│ ├── RenamePrimCommand — Undoable prim rename
│ ├── ReparentPrimCommand — Undoable reparent via UsdNamespaceEditor
│ ├── GroupPrimsCommand — Group selected prims under a new Xform
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes │ ├── AttributeSetCommand — Type-agnostic undoable attribute writes
│ ├── AddReferenceCommand — Undoable reference addition │ ├── AddReferenceCommand — Undoable reference addition
│ ├── ReplaceReferenceCommand — Undoable reference replacement │ ├── ReplaceReferenceCommand — Undoable reference replacement
│ ├── CreateShaderNodeCommand — Undoable UsdShadeShader creation (Material Editor)
│ ├── ConnectShaderAttrsCommand — Undoable shader input↔output connection
│ ├── DisconnectShaderAttrCommand — Undoable shader connection removal
│ └── LayerCommands — Create, remove, reorder sublayers │ └── LayerCommands — Create, remove, reorder sublayers
├── ui/ — ImGui panels and application shell ├── ui/ — ImGui panels and application shell
│ ├── Application — App shell (owns managers/panels, docking, menus) │ ├── Application — App shell (owns managers/panels, docking, menus)
@@ -79,13 +105,21 @@ src/
│ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking) │ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking)
│ ├── SceneHierarchyPanel — Prim tree browser with context menus │ ├── SceneHierarchyPanel — Prim tree browser with context menus
│ ├── PropertyPanel — Attribute/transform editor (channel box-style) │ ├── PropertyPanel — Attribute/transform editor (channel box-style)
│ ├── LayerPanel — Layer stack editor with modal dialogs │ ├── StageEditorPanel — Layer stack editor (edit target, dirty state, drag-drop ordering)
│ ├── TimelinePanel — Transport controls, loop/bounce, frame scrub, Playblast
│ ├── CurveEditorPanel — Maya-style F-curve editor (Bezier ↔ USD time samples)
│ ├── MaterialEditorPanel — Hypershade-style UsdShade node-graph editor
│ ├── MaterialPreviewRenderer — Shader-ball preview: independent Hydra instance + scratch stage
│ ├── NodeThumbnailCache — Async texture decode + amortized shader-ball thumbnails for the node canvas
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale) │ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
── IconManager — SVG icon rasterization via NanoSVG ── IconManager — SVG icon rasterization via NanoSVG
│ └── NodeEditor/ — Vendored/patched imgui-node-editor
└── utils/ — Cross-cutting utilities └── utils/ — Cross-cutting utilities
├── Logger — Thread-safe logging (file + console) ├── Logger — Thread-safe logging (file + console); also sinks USD TF_WARN/TF_ERROR
├── FileDialog — Win32 file open/save dialogs ├── FileDialog — Win32 file open/save dialogs
├── PathUtils — Exe-relative path resolution ├── PathUtils — Exe-relative path resolution
├── OcioConfigParser — Enumerates displays/views/colorspaces/looks from the active OCIO config
├── MovieEncoder — Streams RGBA frames to H.264/MP4 via libavcodec (Playblast)
└── GLExt — GLAD extension initialization └── GLExt — GLAD extension initialization
``` ```
@@ -95,6 +129,7 @@ src/
- **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic - **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic
- **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update) - **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update)
- **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime - **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime
- **Own Color Correction Pass** — Hydra's `HdxColorCorrectionTask` is bypassed; a file-local `ViewportColorCorrector` (inside `UsdSceneRenderer.cpp`) applies sRGB/OCIO as a fullscreen GL pass after Hydra renders linear (see `docs/adr/0001-viewport-color-correction.md`)
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo - **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
## Project Configuration ## Project Configuration
@@ -105,6 +140,7 @@ src/
| `cmake/modules/FindImgui.cmake` | ImGui source integration | | `cmake/modules/FindImgui.cmake` | ImGui source integration |
| `cmake/modules/FindGlad.cmake` | GLAD loader setup | | `cmake/modules/FindGlad.cmake` | GLAD loader setup |
| `CMakePresets.json` | Build presets (VS 17 2022, x64) | | `CMakePresets.json` | Build presets (VS 17 2022, x64) |
| `docs/adr/` | Architecture decision records |
| `AGENTS.md` | AI agent build/architecture guide | | `AGENTS.md` | AI agent build/architecture guide |
| `.kilo/` | Kilo AI configuration (commands, agents, skills) | | `.kilo/` | Kilo AI configuration (commands, agents, skills) |
| `openspec/changes/` | Feature proposals and implementation tracking | | `openspec/changes/` | Feature proposals and implementation tracking |
@@ -123,21 +159,21 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
- **Python 3.12** — OpenUSD requires `python312.dll` at runtime. CMake copies it to the output directory via `POST_BUILD` commands. - **Python 3.12** — OpenUSD requires `python312.dll` at runtime. CMake copies it to the output directory via `POST_BUILD` commands.
- **USD Plugin Path** — The app scans `<exe_dir>/usd/` for `plugInfo.json` and registers plugins via `pxr::PlugRegistry`. - **USD Plugin Path** — The app scans `<exe_dir>/usd/` for `plugInfo.json` and registers plugins via `pxr::PlugRegistry`.
- **Render delegate DLLs** — hdEmbree, hdArnold, and hdCycles each bring their own runtime dependencies (Embree, Arnold SDK, Cycles libs). CMake `POST_BUILD` steps deploy all required DLLs next to the executable automatically.
## Contributing ## Contributing
Before writing any OpenUSD API call, verify the API exists in the actual SDK: Before writing any OpenUSD API call, verify the API exists in the actual SDK:
```powershell ```powershell
findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\ findstr /r /s "FunctionName" third_party\OpenUSD-v25.11\include\
``` ```
Always build and install before running tests: Always build and install before manually verifying a change:
```powershell ```powershell
cmake --build build --config Release cmake --build build --config Release
cmake --install build --config Release cmake --install build --config Release
ctest --test-dir build -C Release
``` ```
## License ## License
+64
View File
@@ -0,0 +1,64 @@
# FindFFmpeg.cmake
# Locates the prebuilt FFmpeg dev package in third_party/ffmpeg.
# Sets up IMPORTED targets: FFmpeg::avcodec FFmpeg::avformat
# FFmpeg::avutil FFmpeg::swscale
# and an umbrella: FFmpeg::FFmpeg
set(FFMPEG_ROOT "${CMAKE_SOURCE_DIR}/third_party/ffmpeg"
CACHE PATH "FFmpeg install root (contains bin/ include/ lib/)")
set(FFmpeg_FOUND TRUE)
foreach(_comp avcodec avformat avutil swscale)
# Import library (.lib) — required for MSVC linking
find_library(FFmpeg_${_comp}_LIB
NAMES ${_comp}
PATHS "${FFMPEG_ROOT}/lib"
NO_DEFAULT_PATH
)
# Runtime DLL — name includes a version suffix, e.g. avcodec-62.dll
file(GLOB _dlls "${FFMPEG_ROOT}/bin/${_comp}-*.dll")
if(_dlls)
list(GET _dlls 0 FFmpeg_${_comp}_DLL)
else()
set(FFmpeg_${_comp}_DLL "")
endif()
if(NOT FFmpeg_${_comp}_LIB)
set(FFmpeg_FOUND FALSE)
message(STATUS "FFmpeg: ${_comp}.lib NOT found in ${FFMPEG_ROOT}/lib")
continue()
endif()
if(NOT FFmpeg_${_comp}_DLL)
set(FFmpeg_FOUND FALSE)
message(STATUS "FFmpeg: ${_comp}-*.dll NOT found in ${FFMPEG_ROOT}/bin")
continue()
endif()
add_library(FFmpeg::${_comp} SHARED IMPORTED)
set_target_properties(FFmpeg::${_comp} PROPERTIES
IMPORTED_IMPLIB "${FFmpeg_${_comp}_LIB}"
IMPORTED_LOCATION "${FFmpeg_${_comp}_DLL}"
INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_ROOT}/include"
)
message(STATUS "FFmpeg: ${_comp} -> ${FFmpeg_${_comp}_LIB}")
endforeach()
if(FFmpeg_FOUND)
# Umbrella target so callers can just link FFmpeg::FFmpeg
if(NOT TARGET FFmpeg::FFmpeg)
add_library(FFmpeg::FFmpeg INTERFACE IMPORTED)
set_target_properties(FFmpeg::FFmpeg PROPERTIES
INTERFACE_LINK_LIBRARIES
"FFmpeg::avcodec;FFmpeg::avformat;FFmpeg::avutil;FFmpeg::swscale"
INTERFACE_INCLUDE_DIRECTORIES
"${FFMPEG_ROOT}/include"
)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(FFmpeg
REQUIRED_VARS FFmpeg_avcodec_LIB FFmpeg_avformat_LIB
FFmpeg_avutil_LIB FFmpeg_swscale_LIB)
+86
View File
@@ -0,0 +1,86 @@
# FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor
#
# This module looks for imgui-node-editor at src/ui/NodeEditor (an in-tree,
# version-controlled subset of the upstream library — MIT licensed, see
# src/ui/NodeEditor/LICENSE — containing only the files this project's build
# actually compiles; the full upstream drop, including the standalone example
# app used to reproduce library-level bugs against a pure reference build,
# stays vendored separately at third_party/imgui-node-editor) and compiles its
# sources directly against this project's own Imgui::Imgui target so it shares
# the exact same ImGui headers/defines (no second copy of ImGui is pulled in).
#
# Inputs:
# IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root
#
# Outputs:
# ImguiNodeEditor_FOUND
# ImguiNodeEditor::ImguiNodeEditor (imported STATIC target)
if(NOT IMGUI_NODE_EDITOR_DIR)
set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/src/ui/NodeEditor")
endif()
if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR)
set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR
"${CMAKE_SOURCE_DIR}/src/ui/NodeEditor/BlueprintWidgets")
endif()
find_path(ImguiNodeEditor_INCLUDE_DIR
NAMES imgui_node_editor.h
PATHS "${IMGUI_NODE_EDITOR_DIR}/Include"
NO_DEFAULT_PATH
)
set(ImguiNodeEditor_SOURCES
"${IMGUI_NODE_EDITOR_DIR}/Source/crude_json.cpp"
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_canvas.cpp"
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor_api.cpp"
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor.cpp"
# Blueprint pin-icon widgets (Widgets/Drawing only — Builders.cpp is
# intentionally excluded, it needs Spring()/BeginHorizontal()/BeginVertical()
# from thedmd's own ImGui fork, which this project's vanilla ImGui lacks).
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Drawing.cpp"
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Widgets.cpp"
)
foreach(_src ${ImguiNodeEditor_SOURCES})
if(NOT EXISTS "${_src}")
set(ImguiNodeEditor_SOURCES_MISSING TRUE)
endif()
endforeach()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ImguiNodeEditor
REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR
FAIL_MESSAGE "imgui-node-editor not found — expected it at src/ui/NodeEditor"
)
if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING)
message(FATAL_ERROR "imgui-node-editor sources missing under ${IMGUI_NODE_EDITOR_DIR}/Source")
endif()
if(ImguiNodeEditor_FOUND AND NOT TARGET ImguiNodeEditor::ImguiNodeEditor)
add_library(imgui_node_editor_impl STATIC ${ImguiNodeEditor_SOURCES})
target_include_directories(imgui_node_editor_impl
PUBLIC "${ImguiNodeEditor_INCLUDE_DIR}"
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Include"
PRIVATE "${IMGUI_NODE_EDITOR_DIR}/Source"
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source"
)
# Must be defined before the first #include <imgui.h> in every translation
# unit (imgui_internal.h hard-errors otherwise) — a command-line define
# guarantees that ordering regardless of vendored include order.
target_compile_definitions(imgui_node_editor_impl PRIVATE IMGUI_DEFINE_MATH_OPERATORS)
# Must share this project's own ImGui build (same headers/defines) rather
# than pulling in a second ImGui — imgui-node-editor only needs the public
# API plus ImRect/ImFloor from imgui_internal.h.
target_link_libraries(imgui_node_editor_impl PUBLIC Imgui::Imgui)
add_library(ImguiNodeEditor::ImguiNodeEditor ALIAS imgui_node_editor_impl)
message(STATUS "imgui-node-editor found: ${ImguiNodeEditor_INCLUDE_DIR}")
endif()
mark_as_advanced(ImguiNodeEditor_INCLUDE_DIR)
+1 -1
View File
@@ -59,6 +59,7 @@ set(OpenUSD_REQUIRED_LIBS
usdGeom usdGeom
usdLux usdLux
usdShade usdShade
usdVol
usdImaging usdImaging
usdImagingGL usdImagingGL
hdx hdx
@@ -71,7 +72,6 @@ set(OpenUSD_REQUIRED_LIBS
hio hio
ar ar
kind kind
ndr
sdr sdr
python python
cameraUtil cameraUtil
+194
View File
@@ -0,0 +1,194 @@
# ADR 0001 — Custom Viewport Color Correction (sRGB / OCIO)
- **Status:** Superseded (2026-06-30) — replaced by the stageView.py approach (HdxColorCorrectionTask via RenderParams)
- **Date:** 2026-06-27
- **Component:** `src/core/UsdSceneRenderer.cpp` (`ViewportColorCorrector`, `ApplyViewportColorCorrection`)
- **Commit:** `fb1ae9d`
## Context
The viewport renders a USD stage through `UsdImagingGLEngine` (Hydra / Storm)
into an offscreen `GlfDrawTarget`, whose color texture ImGui samples for
display. Color management (linear → display) was delegated to Hydra's
`HdxColorCorrectionTask` via `UsdImagingGLEngine::SetColorCorrectionSettings()`,
configured by the per-viewport `ColorCorrectionMode` (Disabled / sRGB /
OpenColorIO) plus OCIO display/view/colorspace/look.
Two problems made the OCIO path unusable:
1. **OCIO output was black.** sRGB correction rendered correctly through the
same color AOV, proving the correction *input* was fine — the hdx OCIO
shader/LUT path itself emitted black. The exact cause stayed opaque because
USD reports it via `TF_WARN`, which this GUI app discarded (no console).
2. **The failure was sticky.** OCIO resources are built lazily *inside*
`Render()` (`HdxColorCorrectionTask::Sync`), not in `SetColorCorrectionSettings()`.
A throw there escaped `UsdSceneRenderer::Render()` and skipped
`m_drawTarget->Unbind()`, permanently unbalancing the `GlfDrawTarget`
bind/restore stack — so every later frame, **including after switching back
to sRGB**, stayed black until an app restart.
We do not control the bundled USD build, so fixing `HdxColorCorrectionTask`
itself was not an option. App and USD both link OpenColorIO 2.1, and a usable
ACES 1.2 config is bundled and pointed to via `$OCIO`.
## Goals / Non-Goals
**Goals:**
- Correct, non-black OCIO display transforms in the viewport (ACES 1.2).
- sRGB output visually identical to the previous hdx sRGB path.
- A color-management failure can never permanently corrupt the viewport.
- Overlays (grid, axis, bbox, camera/light gizmos) remain display-referred,
drawn on top of the corrected image — unchanged from the prior behavior.
**Non-Goals:**
- Replacing Hydra's *rendering* — only its color-correction stage is replaced.
- Exposure/gamma dynamic-property UI, or interactive grading.
- Applying an explicit OCIO look override beyond the view's own looks.
- Color-managing the ImGui UI chrome outside the viewport image.
## Decision
### D1 — Replace `HdxColorCorrectionTask` with our own GL post-process
**Decision:** Hydra always renders **linear** (`colorCorrectionMode = "disabled"`).
A custom fullscreen pass (`ViewportColorCorrector`) applies the correction
afterwards, fully under our control.
**Rationale:** The hdx OCIO path is a black box we cannot patch in the bundled
USD. Owning the post-process gives deterministic, debuggable behavior and
removes the lazy-throw-inside-Render hazard entirely. sRGB and OCIO now share
one code path with consistent ordering.
---
### D2 — Render linear into an `RGBA16F` color attachment
**Decision:** The draw-target color attachment was changed from `GL_RGBA`
(8-bit unorm) to `GL_RGBA16F`.
**Rationale:** We now store the *linear* scene as the intermediate. Display
transforms (especially ACES) need scene values outside `[0,1]`; an 8-bit unorm
buffer would clip highlights and band the shadows before correction even runs.
16F is ample headroom at negligible cost. ImGui and `CaptureFrame` sample/read
it unchanged.
---
### D3 — OCIO via the OCIO GPU API (`GpuShaderDesc`), LUTs uploaded by us
**Decision:** For OCIO mode, build the transform directly with OCIO 2.1:
`config->getProcessor(srcColorSpace, display, view, FORWARD)`
`getDefaultGPUProcessor()``extractGpuShaderInfo(GpuShaderDesc)` with
`GPU_LANGUAGE_GLSL_1_3` and function name `OCIODisplay`. We compile the
generated GLSL into our fragment shader and upload/bind every 1D/2D/3D LUT it
requests as GL textures. The source colorspace defaults to the `scene_linear`
role when unset.
**Rationale:** This is exactly what `HdxColorCorrectionTask` does internally,
but visible and ours to debug. `GLSL_1_3` matches the project's existing
`#version 130` shaders and emits modern `texture()` calls. The program and LUTs
are cached and rebuilt only when display/view/colorspace/look change.
---
### D4 — Graceful degradation to sRGB; never black
**Decision:** Any OCIO failure — processor build, shader compile/link, or LUT
upload — logs the reason and falls back to the sRGB encode shader for that
frame. The failing key is remembered so we do not retry (and re-spam logs)
until the parameters change.
**Rationale:** A misconfigured display/view must degrade to a usable image, not
a black viewport. sRGB is the correct neutral fallback and matches the default.
---
### D5 — Implemented inline in `UsdSceneRenderer.cpp` (pimpl), not new TU/files
**Decision:** `ViewportColorCorrector` is a class defined in
`UsdSceneRenderer.cpp`, held via `std::unique_ptr` behind a forward declaration
in the header.
**Rationale:** `CORE_SOURCES` is a `file(GLOB_RECURSE ...)`; adding a new `.cpp`
forces a CMake reconfigure, which risks re-triggering the heavy Cycles
`ExternalProject` build. Keeping it in an existing TU avoids that while the
pimpl keeps OCIO/GL includes out of the public header. The corrector follows
the file's existing `Init*/Destroy*` GL-resource lifecycle conventions.
---
### D6 — Route USD diagnostics to the app log
**Decision:** Install a process-wide `TfDiagnosticMgr::Delegate`
(`InstallUsdDiagnosticLogger`) that forwards USD `TF_ERROR` / `TF_WARN` /
`TF_STATUS` to the app logger with a `[USD]` prefix.
**Rationale:** USD's own diagnostics were invisible in this GUI app. This made
the original OCIO failure undiagnosable and is generally useful for any Hydra
issue.
## Pipeline
Per frame, after Hydra renders linear into the draw target:
1. `m_drawTarget->Resolve()` — resolves MSAA into the color attachment (no-op
when MSAA is off), giving a single-sample linear image.
2. Blit that attachment into `m_ccLinearTex` — a separate copy is required
because sampling and writing the same texture in one pass is illegal.
3. Bind the draw-target render FBO (the MSAA FBO when multisampling) and draw
the corrected fullscreen triangle sampling `m_ccLinearTex`.
4. Overlays draw on top (uncorrected), then the final `Resolve()` in
`GetColorTextureID()` produces the texture ImGui samples.
This keeps overlays display-referred exactly as before, and the correction is
MSAA-correct (a flat fullscreen quad resolves to the same value per sample).
## Consequences
**Positive:**
- OCIO display transforms work; OCIO failures degrade to sRGB, never black.
- A color-correction failure can no longer corrupt the bind stack — the throw
hazard is gone because Hydra no longer does OCIO.
- The full transform is inspectable/loggable in our own code.
- HDR-linear intermediate enables correct tone-mapped output.
**Negative / trade-offs:**
- We reimplement and now maintain OCIO GPU plumbing (LUT upload, sampler
binding) that USD would otherwise own; OCIO major-version API changes could
require updates.
- One extra fullscreen pass, one blit, and an `RGBA16F` buffer per viewport.
- OCIO dynamic properties (exposure/gamma) and explicit look overrides are not
wired up.
- Logic lives in `UsdSceneRenderer.cpp` rather than its own module, by D5.
## Alternatives Considered
- **Keep `HdxColorCorrectionTask`, fix configuration.** Rejected: the failure
was inside the bundled USD's GPU/LUT integration, not our parameters; we
cannot patch it, and sRGB-correct/OCIO-black pointed at the hdx shader path.
- **Custom sRGB only, keep hdx for OCIO.** Rejected: OCIO is the actual
requirement; a custom sRGB-only path wouldn't address it.
- **Bake the OCIO transform to a single 3D LUT on the CPU.** Rejected: loses
HDR shaper handling and precision that `GpuShaderDesc` manages correctly.
- **New `ViewportColorCorrection.{h,cpp}` module.** Rejected for now (D5) to
avoid a CMake reconfigure / Cycles rebuild; revisit if the file grows.
## Verification
Built clean (Release) and confirmed in-app by the maintainer: OCIO produces the
expected corrected (non-black) image and sRGB is unchanged. Headless automated
verification is blocked by the stale test-target CMake config (missing USD
include dirs); restoring those targets would let an OCIO-vs-sRGB center-pixel
test assert this automatically.
## Supersession Note (2026-06-30)
This ADR is superseded. The custom `ViewportColorCorrector` was removed in favour
of the approach used in usdview's `stageView.py`: `colorCorrectionMode`,
`ocioDisplay`, `ocioView`, `ocioColorSpace`, and `ocioLook` are now passed
directly to `UsdImagingGLRenderParams` and `SetColorCorrectionSettings()`,
delegating correction to Hydra's `HdxColorCorrectionTask`. The two failure modes
documented in D1 (black OCIO output, bind-stack corruption) may resurface; the
try/catch guard around `Render()` remains in place to mitigate the bind-stack
issue.
+315
View File
@@ -0,0 +1,315 @@
# ADR 0002 — Render Layer 面板(Maya 風格,建構於 USD Sublayer 架構之上)
- **Status:** Proposed(尚未實作)
- **Date:** 2026-07-12
- **Component:** `src/core/RenderLayerManager`(新)、`src/ui/RenderLayerPanel`(新)、
`src/core/commands/RenderLayerCommands`(新)、
`src/core/commands/ConnectShaderAttrsCommand`
`src/core/commands/DisconnectShaderAttrCommand``src/ui/MaterialEditorPanel`
`src/core/UsdStageManager``src/ui/StageEditorPanel`
`src/ui/SceneHierarchyPanel``src/ui/Application`
- **Commit:** (尚未實作,無對應 commit)
## Context
使用者希望在本應用中加入一個「Render Layer」面板,行為比照 Maya 的
Render Layers:使用者可以把場景中的燈光與可渲染物件加入某個具名圖層,
該圖層記錄對這些物件的屬性調整——包含 shading 連線的修改——而不影響
場景的基礎資料;切換目前作用中的圖層時,viewport 套用該圖層的所有覆寫,
並且**只顯示該圖層的成員燈光與物件**;預設圖層(Default)則等同於目前
未經任何 render layer 修改的 base stage。
為了先弄清楚該怎麼設計,再動手實作,我們先派出三個平行的研究/探索
subagent:一個研究 Maya Render Layers(含舊版 Render Layers 與後續的
Render Setup)的實際資料模型與行為語意;一個深入探索本專案既有的
layer/stage/command 架構(`LayerManager``UsdStageManager`
`CommandHistory`、既有的 command 範本、`PropertyManager`
`StageEditorPanel`);一個探索 viewport 可見度控制與既有的材質覆寫機制
(`UsdSceneRenderer`/`UsdImagingGLEngine``SceneHierarchyPanel`
眼睛圖示隱藏機制、`MaterialEditorPanel` 的材質綁定與 shader 連線
command)。三份研究都完成後,再交由一個 Plan agent 讀取實際原始碼,
驗證並修正整體設計——這個過程中抓到兩個既有程式碼裡的真實正確性問題
(見下方 D3、D4)。本 ADR 記錄這輪研究與規劃後定案的設計決策。
**與真實 Maya 行為的刻意偏離:** Maya 切換 render layer 預設**不會**
自動 isolate viewport——那只是純粹的 render-time/override-time 概念,
非成員物件只有在該圖層明確加了 visibility override 時才會被隱藏。
但本專案的使用者明確要求「切換到圖層時,只顯示該圖層內的燈光與物件」,
這是經與使用者確認過的刻意設計,而非誤解 Maya 行為。
## Goals / Non-Goals
**Goals:**
- 非破壞性地記錄每個圖層的屬性覆寫(未來階段含 shading connection 與
material binding 覆寫),不修改場景的基礎資料。
- 切換作用中圖層時,viewport 即時反映該圖層的所有覆寫。
- 切換作用中圖層時,viewport 只顯示該圖層的成員燈光/物件(isolate)。
- 正確的存檔/重新開檔 round-trip(圖層清單、成員資格、覆寫內容、目前
作用中的圖層都要能還原)。
- 圖層切換與圖層內的編輯都要能正確 Undo/Redo。
**Non-Goals(本階段,MVP 範圍之外,留待後續 Phase):**
- Shading connection 覆寫的使用者介面(底層機制在 Phase 0 就會修正到
可正確運作,但本階段不在 Render Layer 面板上開放這個操作)。
- Material binding(材質指派)覆寫的使用者介面。
- 每圖層覆寫清單的檢視/還原(revert)UI,比照 Maya Render Setup 的
Property Editor 覆寫堆疊視圖。
- 從 Scene Hierarchy 拖放物件到 Render Layer 面板做成員編輯(MVP 先用
「加入所選」按鈕取代)。
這些項目會在後續 Phase(見 Consequences 與另一份實作計畫
`misty-watching-stardust.md`)補齊,此處先列出以留紀錄,避免日後誤以為
被遺漏。
## Decision
### D1 — 每個 Render Layer 對應一個獨立、檔案化的 `SdfLayer`
**決策:** 每建立一個 render layer,就建立一個實體(檔案化,而非
anonymous)的 `SdfLayer`,掛載為 stage root layer 的 sublayer,並以
`SdfLayer::SetCustomLayerData` 標記
(例如 `usdLayerManager:renderLayer = true`
`usdLayerManager:renderLayerName = "<名稱>"`),藉此與一般內容 sublayer
區分。絕對不可放進 session layer。
**理由:** `UsdStageManager::MergeSessionLayerIntoRoot()` 會在每次存檔
時把 session layer 的內容攤平合併進 root layer、再清空 session
layer——若 render layer 的資料放在 session layer,存檔當下就會被摧毀。
另外,anonymous `SdfLayer` 的 identifier(`anon:0x...`)只在執行期有效,
寫進 `subLayerPaths` 之後無法在 `SaveStageAs`/重新開檔時被正確解析,
因此每個 render layer 必須是真正的檔案(沿用
`LayerManager::CreateNewSublayer` 既有的「建立檔案 → 存檔 → 插入
sublayer」流程)。
---
### D2 — Default 圖層是虛擬的,沒有對應的 `SdfLayer`
**決策:** 「Default」不是一個真正的 render layer 物件,選取它只代表
「把所有 render-layer sublayer 全部靜音(mute)」。
**理由:** 這正好等同於今天應用程式既有的行為——沒有任何 render layer
生效時,viewport 顯示的就是未經修改的 base stage。不需要為 Default
另外設計一套資料結構或程式碼路徑。
---
### D3 — 成員資格以 `UsdCollectionAPI` 表示,並修正「非現用圖層無法讀寫」的問題
**決策:** 每個 render layer 的成員資格,用 `UsdCollectionAPI`
(instance name 固定為 `"members"`)表示,掛在該圖層自己 `SdfLayer`
內部一個固定保留路徑(`/RenderLayerData`)的 `over`-only admin prim
上。這個 prim 只有 `over`、沒有任何 `def`,所以是合法、可定址的
`UsdPrim`,但 `IsDefined() == false`,不會出現在
`UsdStage::Traverse()`、Scene Hierarchy 面板、或
`PropertyManager::GetPrimPaths()` 的預設列舉結果中,不需要額外過濾。
規劃過程中發現一個關鍵問題:由於同一時間只有一個 render layer 是
「現用」(unmuted)的,其餘全部處於 muted 狀態,而
`UsdStage::MuteLayer` 會讓該圖層**完全**退出 stage 的合成
(composition)——也就是說,對一個目前被 mute 的圖層呼叫
`stage->GetPrimAtPath("/RenderLayerData")` 會拿到一個**無效**的
`UsdPrim`,`Usd` 層級的 `UsdCollectionAPI` 便完全無法讀寫該圖層的成員
資料(例如使用者想在 Render Layer 面板編輯一個目前非現用圖層的成員
清單時)。
**修正做法:** 對於非現用圖層的成員資格讀寫,一律改走**Sdf 層級 API**,
直接對該圖層的 `SdfLayerHandle` 操作(`SdfCreatePrimInLayer`
建立/取得 `/RenderLayerData` 的 prim spec,再用 `SdfRelationshipSpec`
操作 `collection:members:includes` 這個 relationship 的
target path 清單)——這條路徑完全不經過 stage 合成,因此不受 mute
狀態影響,而且寫入的 opinion 名稱與 `UsdCollectionAPI` 完全一致。
只有在計算「目前現用圖層」的 isolate 用成員集合時,才使用一般
`Usd` 層級 API(`UsdCollectionAPI::GetCollection` +
`ComputeMembershipQuery` + `UsdComputeIncludedObjectsFromCollection`),
因為此時該圖層必然已經是 unmuted 的。兩條路徑存取的是同一份
authored opinion,只是兩種不同的入口,不是兩套 schema。
---
### D4 — 重用既有的 edit-target 機制做屬性覆寫,並修正三個既有的正確性缺陷
**決策:** 切換作用中的 render layer 時,把 stage 的
ambient edit target 指向該圖層的 `SdfLayer`(沿用
`LayerManager::SetEditTarget`)。
**理由與驗證:** 重新閱讀 `PropertyManager.cpp``TransformCommand`
後確認,`PropertyManager::SetPropertyValue`(Property Panel 一般屬性
編輯的路徑)與 `TransformCommand`(viewport 操作桿與 Property Panel
的 Transform 編輯路徑)都是在編輯提交的當下,從 stage 目前的
ambient edit target 解析要寫入哪個 `SdfLayer`。因此,只要把 ambient
edit target 換成 render layer 的 `SdfLayer`,這兩條既有的寫入路徑
**完全不需要修改任何程式碼**,就會自動把編輯正確記錄到對應的 render
layer 裡。
但在驗證過程中,發現另外三個既有的寫入路徑目前**沒有**捕捉/套用
`UsdEditContext`,而是在 `Execute()`/`Undo()` 真正執行的當下才讀取
ambient edit target:`ConnectShaderAttrsCommand`
`DisconnectShaderAttrCommand`、以及
`MaterialEditorPanel::BindMaterialToTarget` 的材質綁定/解除綁定
closure。具體會出錯的情境:使用者在 Render Layer A 生效時連接了一條
shader 連線(此時正確寫入 A);接著切換到 Render Layer B(ambient
edit target 也隨之換成 B);此時按下復原(Undo),`Undo()` 會在
**B** 這個圖層上執行「移除連線」,而不是回到原本執行 `Execute()`
時的 A——結果是 A 留下一條沒被正確復原的連線 opinion,B 卻多了一條
不該存在的覆寫。這其實是既有程式碼中已經存在、但目前很少被觸發到的
潛在 bug(任何在「連線」與「復原連線」之間發生 edit target 變更的
情境都會中招),必須在導入 render layer 之前先修正,否則問題會被
render layer 的切換行為放大成使用者能輕易踩到的日常錯誤。
**修正做法:** 比照 `TransformCommand` 既有的寫法,為
`ConnectShaderAttrsCommand``DisconnectShaderAttrCommand`
新增一個建構參數 `SdfLayerHandle editLayer`,並在 `Execute()`/
`Undo()` 內以 `UsdEditContext(m_stage, m_editLayer)`
包裹實際的連線/解除連線呼叫;`MaterialEditorPanel` 在建立這些
command 與綁定/解除綁定的 closure 時,一併在建構當下捕捉
`stage->GetEditTarget().GetLayer()` 並傳入。這個修正不影響現有
(尚無 render layer 時)的行為——沒有明確傳入圖層時,維持原本讀取
ambient edit target 的行為不變。
---
### D5 — Viewport Isolate 採用「authored visibility opinion」,而非重建渲染引擎或整個 stage 的 population mask
**決策:** 圖層啟用、或該圖層成員資格變動時,對場景做一次由上而下的
遍歷:若某個子樹完全不含任何成員,就在該子樹**最上層**的 prim 上,
`UsdGeomImageable`(與 Scene Hierarchy 面板既有的眼睛圖示隱藏
機制完全相同的原語)authoring 一個 `visibility = invisible`,並且
**停止往下遞迴**(子孫節點靠繼承取得隱藏效果);若子樹內含成員,則不
寫入任何東西、繼續往下遞迴。規則上永遠不主動 authoring
`visible`——只利用預設的「inherited」語意,避免蓋掉場景中原本就刻意
隱藏的物件。所有自動寫入的路徑,記錄在該圖層自己的
`customLayerData`(例如 `usdLayerManager:autoHiddenPaths`)中,
以便下次重新計算前可以精準只清除這些自動產生的 opinion,不誤刪
使用者手動authoring 的覆寫。
**理由(排除的替代方案見下方 Alternatives Considered):**
重新閱讀
`third_party/OpenUSD-v25.05/include/pxr/usdImagingGL/engine.h`
確認,`UsdImagingGLEngine::Parameters::invisedPaths`
只能在**建構時**指定,整個類別沒有任何執行期可用的
setter——每次切換圖層都重建 engine 的代價太高(會丟失 Hydra
scene-index 狀態,以及 Arnold/Cycles/Embree 等漸進式渲染委派已累積
的取樣結果,並造成 viewport 明顯的畫面閃爍)。而
`UsdStage::SetPopulationMask()`/`SetLoadRules()`
雖然可在執行期呼叫,但作用範圍是整個 stage 的合成,會連帶讓
Scene Hierarchy 面板、Property Panel 的選取、viewport 的
pick 都看不到/選不到非成員物件——這遠超出「只讓 viewport
isolate」的需求,等於把物件從整個場景圖裡「拿掉」而非「隱藏」。
以真正的 authored opinion 達成 isolate,則是可檢視、可除錯的
USD 資料(能在 Stage Editor 等既有工具中直接看到),而且已經證實
Hydra 能對這類編輯即時反應而不需要重建 engine(既有的 Property
Panel 編輯、Scene Hierarchy 的眼睛圖示隱藏,都是同一套機制)。
開銷與被隱藏的「子樹根節點」數量成正比,而非與場景總物件數成正比,
對一般場景規模而言可忽略。
---
### D6 — 圖層切換本身也是一個可 Undo 的操作
**決策:** 新增 `RenderLayerActivateCommand`,和其他編輯一樣推入
`CommandHistory`。建構時就先記錄「切換前的 edit target」,以便
Undo 或切回 Default 時正確還原。
**理由:** 切換圖層會 authoring 真正的狀態(isolate 用的 visibility
opinion、edit target 的變更),如果不納入 Undo 堆疊,使用者連續按
Ctrl+Z 時會「跳過」圖層切換這一步,卻仍然復原了在該圖層內做的編輯,
造成不一致、難以理解的復原歷史。此設計已與使用者確認。
---
### D7 — 圖層生效期間,鎖定既有的 edit-target 相關 UI
**決策:** 當某個非 Default 的 render layer 是現用狀態時,停用
Stage Editor 面板的 edit-target checkbox,以及 Scene Hierarchy
面板的圖層下拉選單,並加上提示文字說明原因。
**理由:** 這兩個既有的 UI 都是直接讀寫 stage 的 ambient edit
target。如果放任使用者在 render layer 生效期間透過這些既有 UI
把 edit target 改到別的地方,「編輯會正確落在目前作用中的 render
layer」這個核心不變量就會被悄悄破壞,且沒有任何提示。鎖定雖然犧牲
一些彈性,但能避免使用者在不知情的狀況下把覆寫寫錯地方。此設計已
與使用者確認(相對於「保留可操作、只顯示警告」的替代方案)。
---
### D8 — 存檔與重新開檔的持久化
**決策:** `UsdStageManager::SaveStage`/`SaveStageAs`
除了既有的存檔呼叫之外,額外明確逐一存下**每一個** render-layer
`SdfLayer`(不論目前是否被 mute)。哪個圖層目前是「現用」這件事,
額外以 root layer 的 custom layer data 記錄
(例如 `usdLayerManager:activeRenderLayer`),並在
`Application::RefreshManagers()`/開啟 stage 時讀回,重新套用對應的
`MuteLayer`/`UnmuteLayer``SetEditTarget`
**理由:** `UsdStage::Save()`
只會走訪目前在合成(composition)中的 layer 存檔,而同一時間必然有
N-1 個 render-layer sublayer 處於 muted、不在合成中的狀態——若不
額外處理,這些圖層的編輯內容在存檔時會被靜默遺漏。另外,mute 狀態
本身只是 `UsdStage` 的執行期旗標,並不會被序列化進任何檔案,因此
「目前是哪個圖層生效」這件事也必須另外顯式持久化,否則重新開檔後
會遺失,退回成 Default 生效的狀態。
## Consequences
**正面:**
- 幾乎完全重用既有的 `SdfLayer`/`UsdEditContext`/`CommandHistory`/
`LayerManager` 機制,不需要引入新的渲染或合成機制,實作與既有
程式碼風格高度一致。
- 每個圖層的覆寫都是真正、可檢視的 USD opinion,能在 Stage Editor
等既有工具中直接檢視、除錯,不是一個只存在於應用程式記憶體裡的
「影子」狀態。
- Isolate 邏輯的開銷與「被隱藏的子樹根節點數」成正比,不隨場景總
物件數線性增加。
- 圖層切換、圖層內編輯,都能正確納入既有的 Undo/Redo 體系,使用者
操作心智模型一致。
**負面/取捨:**
- 成員資格需要維護兩條存取路徑(現用圖層走 `Usd` 層級 API、非現用
圖層走 `Sdf` 層級 API),增加一些實作複雜度與日後維護成本。
- 鎖定既有 edit-target UI 犧牲了一部分操作彈性。
- 每個 render layer 都對應一個實體檔案(而非 anonymous layer),
會在專案目錄下產生額外的附屬檔案,需要一致的檔名/存放規則。
- 本階段(MVP)尚未支援 shading connection、material binding 的
每圖層覆寫 UI,也還沒有覆寫清單的檢視/還原介面——這些留待後續
Phase,在此之前 Render Layer 面板的覆寫能力僅限一般屬性數值與
Transform。
- `ConnectShaderAttrsCommand`/`DisconnectShaderAttrCommand`/
`BindMaterialToTarget` 的修正屬於既有程式碼的行為調整,雖然設計上
刻意保持向下相容(未傳入圖層時行為不變),仍需要在導入 render
layer 之前完成並個別驗證,不能與 render layer 本身的功能驗證
混在一起。
## Alternatives Considered
- **用 `UsdStage::SetPopulationMask()`/`SetLoadRules()` 做 isolate。**
否決:這兩者作用在整個 stage 的合成層級,會讓 Scene Hierarchy
面板、Property Panel 的選取與 viewport 的 pick 一併看不到/選不到
非成員物件,遠超出「只讓 viewport isolate」的實際需求。
- **用 `UsdImagingGLEngine` 建構期的 `invisedPaths`,每次切換圖層就
重建 engine。** 否決:代價過高——會丟失 Hydra scene-index 狀態,
以及漸進式渲染委派(Arnold/Cycles/Embree)已累積的取樣結果,並造成
viewport 明顯閃爍,不適合互動式的頻繁圖層切換。
- **用 anonymous `SdfLayer` 存放每個 render layer 的資料。**
否決:anonymous layer 的 identifier 只在執行期有效,寫進
`subLayerPaths` 後,在 `SaveStageAs`/重新開檔時無法被正確解析,
資料會在存檔/重載之間遺失。
- **用 `UsdVariantSet` 表示 render layer 之間互斥的選擇。**
否決:Maya 的 collection + override 模型本質上更接近可疊加、
可獨立排序的 sublayer/override 合成,而非彼此互斥、一次只能選一
個的 variant 切換,語意上不夠貼合(尤其考慮到未來允許同一物件
同時屬於多個圖層的情境)。
## Verification
本 ADR 記錄的是**實作前**的設計決策——目前尚未進行任何程式碼變更,
因此本節暫不回報建置或手動驗證結果。待對應的實作計畫
(`misty-watching-stardust.md` 中記錄的 Phase 0/Phase 1 MVP)實際
執行完成後,應在本節或另外新增一則後續紀錄中,補上實際的建置結果與
手動驗證步驟/結果(圖層建立與切換、viewport isolate 是否正確、
屬性覆寫是否正確落在對應圖層且不外洩到其他圖層、存檔重新開檔的
round-trip、跨圖層切換的 Undo/Redo 行為),寫法比照
`0001-viewport-color-correction.md` 文末的「Supersession Note」
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
決策當下的紀錄。
Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

-62
View File
@@ -1,62 +0,0 @@
[Window][WindowOverViewport_11111111]
Pos=0,22
Size=1920,1093
Collapsed=0
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Scene Hierarchy]
Pos=0,22
Size=285,655
Collapsed=0
DockId=0x00000001,0
[Window][Layer Panel]
Pos=60,60
Size=121,48
Collapsed=0
[Window][Viewport]
Pos=287,22
Size=1286,1003
Collapsed=0
DockId=0x00000007,0
[Window][Property Panel]
Pos=1575,22
Size=345,1093
Collapsed=0
DockId=0x00000006,0
[Window][Stage Info]
Pos=0,22
Size=285,655
Collapsed=0
DockId=0x00000001,1
[Window][Stage Editor]
Pos=0,679
Size=285,436
Collapsed=0
DockId=0x00000002,0
[Window][Timeline]
Pos=287,1027
Size=1286,88
Collapsed=0
DockId=0x00000008,0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,22 Size=1920,1093 Split=X
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=1573,637 Split=X
DockNode ID=0x00000003 Parent=0x00000005 SizeRef=285,637 Split=Y Selected=0xB8729153
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=289,381 Selected=0xB8729153
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=289,254 Selected=0xBE78FE5F
DockNode ID=0x00000004 Parent=0x00000005 SizeRef=1286,637 Split=Y Selected=0xC450F867
DockNode ID=0x00000007 Parent=0x00000004 SizeRef=850,547 CentralNode=1 Selected=0xC450F867
DockNode ID=0x00000008 Parent=0x00000004 SizeRef=850,88 Selected=0x4F89F0DC
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=345,637 Selected=0xFA2314A6
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M41.4 233.4c12.5-12.5 32.8-12.5 45.3 0L256 402.7 425.4 233.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0l-192-192c-12.5-12.5-12.5-32.8 0-45.3z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>

After

Width:  |  Height:  |  Size: 460 B

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eye-slash.svg"
width="512"
height="512"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="319.92833"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M 45.154688,63.56677 C 37.354061,57.416274 26.02815,58.841389 19.877654,66.642017 13.727159,74.442646 15.152273,85.768558 22.952903,91.919053 L 466.98865,439.94706 c 7.80061,6.1505 19.12653,4.72539 25.27703,-3.07524 6.15049,-7.80063 4.72538,-19.12654 -3.07524,-25.27704 l -78.90636,-61.80497 c 29.7024,-30.45246 49.80401,-64.5802 59.92982,-88.80716 2.4752,-5.92547 2.4752,-12.526 0,-18.45147 -11.17589,-26.77716 -34.65278,-65.7803 -69.75561,-98.33292 -35.25284,-32.85264 -83.78174,-60.454866 -144.38662,-60.454866 -51.15412,0 -93.75756,19.726586 -126.98523,45.603666 z M 183.39082,171.87549 c 19.12653,-17.47641 44.70359,-28.12727 72.68085,-28.12727 59.6298,0 108.0087,48.37889 108.0087,108.0087 0,18.6765 -4.72539,36.22791 -13.05107,51.52914 l -28.95232,-22.65182 c 6.30051,-14.47616 7.95063,-31.05249 3.60029,-47.47882 -8.32567,-31.1275 -35.85288,-52.05419 -66.45536,-53.3293 -4.35034,-0.15001 -6.90055,4.57537 -5.55044,8.77572 1.57513,4.80038 2.4752,9.90078 2.4752,15.22622 0,7.65061 -1.80014,14.85119 -4.9504,21.22671 L 183.46581,171.95049 Z M 295.82487,352.19 c -12.30099,4.87539 -25.72707,7.57562 -39.7532,7.57562 -59.62981,0 -108.0087,-48.3789 -108.0087,-108.0087 0,-5.17542 0.37503,-10.20082 1.05009,-15.15122 L 78.382365,180.87621 c -17.101377,22.27679 -29.327363,44.47858 -36.452936,61.65497 -2.475199,5.92547 -2.475199,12.526 0,18.45147 11.175899,26.77717 34.652789,65.7803 69.755611,98.33293 35.25284,32.85264 83.78174,60.45487 144.38663,60.45487 35.85289,0 67.43042,-9.67578 94.65761,-24.37697 z"
id="path1"
style="stroke-width:0.75006" />
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eye.svg"
width="512"
height="512"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="287.82083"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 250.94921,80.734602 c -63.04963,0 -113.53614,28.715668 -150.28908,62.893558 -36.518844,33.86576 -60.942769,74.44226 -72.491463,102.29957 -2.575046,6.16451 -2.575046,13.0313 0,19.1958 11.548694,27.85732 35.972619,68.43382 72.491463,102.29958 36.75294,34.17789 87.23945,62.89356 150.28908,62.89356 63.04962,0 113.53613,-28.71567 150.28907,-62.89356 36.51884,-33.94379 60.94277,-74.44226 72.56949,-102.29958 2.57505,-6.1645 2.57505,-13.03129 0,-19.1958 C 462.18105,218.07042 437.75712,177.49392 401.23828,143.62816 364.48534,109.45027 313.99883,80.734602 250.94921,80.734602 Z M 138.58355,255.52563 a 112.36566,112.36566 0 1 1 224.73132,0 112.36566,112.36566 0 1 1 -224.73132,0 z m 112.36566,-49.94029 c 0,27.54519 -22.39511,49.94029 -49.9403,49.94029 -5.54025,0 -10.84641,-0.93637 -15.84043,-2.57505 -4.29175,-1.40456 -9.28578,1.24851 -9.12972,5.77436 0.2341,5.38418 1.01441,10.76837 2.49702,16.15256 10.69034,39.95223 51.81305,63.67387 91.76529,52.98353 39.95224,-10.69034 63.67387,-51.81305 52.98353,-91.76529 -8.66152,-32.38316 -37.29916,-54.15401 -69.13609,-55.48055 -4.52584,-0.15607 -7.17892,4.75993 -5.77435,9.12971 1.63866,4.99403 2.57505,10.30019 2.57505,15.84044 z"
id="path1"
style="stroke-width:0.780317" />
</svg>

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 2.4 KiB

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM216 232l0 48 48 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-48 0 0 48c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-48-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0 0-48c0-13.3 10.7-24 24-24s24 10.7 24 24z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 64 64"
version="1.1"
id="svg1"
sodipodi:docname="file-plus.svg"
width="64"
height="64"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="13.953125"
inkscape:cx="30.351624"
inkscape:cy="32.788354"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 18.366884,5.8051512 c -3.568558,0 -6.469907,2.9085663 -6.469907,6.4860018 v 38.916014 c 0,3.577435 2.901349,6.486002 6.469907,6.486002 h 25.879632 c 3.568559,0 6.469908,-2.908567 6.469908,-6.486002 V 22.020157 H 37.776609 c -1.789335,0 -3.234954,-1.449217 -3.234954,-3.243002 V 5.8051512 Z m 19.409725,0 V 18.777155 H 50.716424 Z M 33.732916,29.316909 v 4.864502 h 4.852431 c 1.344528,0 2.426216,1.084379 2.426216,2.43225 0,1.347873 -1.081688,2.432252 -2.426216,2.432252 h -4.852431 v 4.864501 c 0,1.347872 -1.081688,2.43225 -2.426216,2.43225 -1.344527,0 -2.426215,-1.084378 -2.426215,-2.43225 v -4.864501 h -4.852431 c -1.344528,0 -2.426215,-1.084379 -2.426215,-2.432252 0,-1.347871 1.081687,-2.43225 2.426215,-2.43225 h 4.852431 v -4.864502 c 0,-1.347872 1.081688,-2.43225 2.426215,-2.43225 1.344528,0 2.426216,1.084378 2.426216,2.43225 z"
id="path1"
style="stroke-width:0.101217" />
</svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 2.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="white" d="M495.9 166.6c3.2 8.7.5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8C283.7 510.8 269.5 512 255 512s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4l-55.7 17.7c-8.8 2.8-18.6.3-24.5-6.8-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1C25.1 368.8 19.8 357.4 15.4 345.5c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.0 273.1 63.4 264.6 63.4 256s.6-17.1 1.7-25.4L21.8 191.2c-6.9-6.2-9.6-15.9-6.4-24.6 4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2 5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1C196.5 12.3 203.5 5.1 212.7 3.6 226.5 1.2 240.7 0 255.2 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8 8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM255.2 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+37 -1
View File
@@ -1 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 32C0 14.3 14.3 0 32 0L160 0c17.7 0 32 14.3 32 32l0 384c0 53-43 96-96 96s-96-43-96-96L0 32zM223.6 425.9c.3-3.3 .4-6.6 .4-9.9l0-262 75.4-75.4c12.5-12.5 32.8-12.5 45.3 0l90.5 90.5c12.5 12.5 12.5 32.8 0 45.3L223.6 425.9zM182.8 512l192-192L480 320c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-297.2 0zM128 64L64 64l0 64 64 0 0-64zM64 192l0 64 64 0 0-64-64 0zM96 440a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="swatchbook.svg"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="255.71333"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 36.12094,73.137738 c 0,-14.449385 12.282308,-26.12318 27.484883,-26.12318 H 173.54535 c 15.20258,0 27.48489,11.673795 27.48489,26.12318 V 386.6159 c 0,43.26652 -36.93281,78.36954 -82.45465,78.36954 -45.521839,0 -82.45465,-35.10302 -82.45465,-78.36954 z M 228.17156,394.69776 c 0.25766,-2.69395 0.34356,-5.38791 0.34356,-8.08186 V 172.73237 l 64.76125,-61.55275 c 10.73629,-10.20437 28.172,-10.20437 38.90828,0 l 77.7307,73.87962 c 10.73629,10.20436 10.73629,26.77626 0,36.98062 z m -35.04323,70.28768 164.9093,-156.73908 h 90.35655 c 15.20257,0 27.48488,11.6738 27.48488,26.12318 v 104.49273 c 0,14.44938 -12.28231,26.12317 -27.48488,26.12317 z M 146.06047,99.260915 H 91.090705 V 151.50728 H 146.06047 Z M 91.090705,203.75364 V 256 h 54.969765 v -52.24636 z m 27.484885,202.45465 a 20.613662,19.592386 0 1 0 0,-39.18477 20.613662,19.592386 0 1 0 0,39.18477 z"
id="path1"
style="stroke-width:0.837356" />
</svg>

Before

Width:  |  Height:  |  Size: 693 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.
Binary file not shown.
+241
View File
@@ -0,0 +1,241 @@
#include "MaterialManager.h"
#include <pxr/usd/sdr/registry.h>
#include <pxr/usd/sdr/shaderProperty.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/nodeGraph.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/usd/usdShade/utils.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h>
#include <algorithm>
#include <cctype>
#include <set>
namespace UsdLayerManager {
namespace {
// Coarse Hypershade-style bucket for the create-node list. Sdr metadata is
// parser-dependent (glslfx vs MaterialX vs Arnold vs Cycles fill different
// fields), so classify from the strongest signal available: context first,
// then terminal output types, then role, then name keywords.
std::string DeriveCategory(pxr::SdrShaderNodeConstPtr node) {
const std::string context = pxr::TfStringify(node->GetContext());
if (context == "surface" || context == "volume" || context == "displacement")
return "Material";
if (context == "light" || context == "lightFilter")
return "Light";
// MaterialX/Arnold terminals often lack a context but type their outputs
// as surfaceshader/volumeshader/displacementshader/closure.
for (const pxr::TfToken& outName : node->GetShaderOutputNames()) {
if (const auto* prop = node->GetShaderOutput(outName)) {
const std::string outType = pxr::TfStringify(prop->GetType());
if (outType.find("shader") != std::string::npos ||
outType == "terminal" || outType == "closure")
return "Material";
}
}
const std::string role = pxr::TfStringify(node->GetRole());
if (role == "texture") return "Texture";
if (role == "primvar") return "Geometry";
if (role == "math") return "Utility";
const std::string name = pxr::TfStringToLower(
node->GetIdentifier().GetString() + " " + node->GetName());
for (const char* kw : {"texture", "image", "checker", "noise", "ramp",
"fractal", "worley", "voronoi", "triplanar"})
if (name.find(kw) != std::string::npos) return "Texture";
for (const char* kw : {"primvar", "texcoord", "geomprop", "position",
"normal", "tangent", "bitangent", "uv_"})
if (name.find(kw) != std::string::npos) return "Geometry";
return "Utility";
}
// Fixed display order for the known buckets; anything unexpected sorts after.
int CategoryRank(const std::string& category) {
if (category == "Material") return 0;
if (category == "Texture") return 1;
if (category == "Geometry") return 2;
if (category == "Utility") return 3;
if (category == "Light") return 4;
return 5;
}
// Top-level group from the Sdr source type. glslfx and OSL collapse into one
// "USD" group — the core usd* nodes register under both parsers and would
// otherwise show up twice under different headers.
std::string DeriveSource(pxr::SdrShaderNodeConstPtr node) {
std::string st = pxr::TfStringToLower(pxr::TfStringify(node->GetSourceType()));
if (st == "glslfx" || st == "osl") return "USD";
if (st == "mtlx") return "MaterialX";
if (st == "arnold") return "Arnold";
if (st == "cycles") return "Cycles";
if (st.empty()) return "Other";
st[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(st[0])));
return st;
}
int SourceRank(const std::string& source) {
if (source == "USD") return 0;
if (source == "MaterialX") return 1;
return 2; // renderers and anything else, alphabetical among themselves
}
} // namespace
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
if (m_shaderNodeCacheBuilt)
return m_shaderNodeCache;
for (pxr::SdrShaderNodeConstPtr node : pxr::SdrRegistry::GetInstance().GetShaderNodesByFamily()) {
if (!node) continue;
ShaderNodeTypeInfo info;
info.identifier = node->GetIdentifier().GetString();
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
info.family = node->GetFamily().GetString();
info.category = DeriveCategory(node);
info.source = DeriveSource(node);
m_shaderNodeCache.push_back(std::move(info));
}
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
const int sa = SourceRank(a.source), sb = SourceRank(b.source);
if (sa != sb) return sa < sb;
if (a.source != b.source) return a.source < b.source;
const int ra = CategoryRank(a.category), rb = CategoryRank(b.category);
if (ra != rb) return ra < rb;
if (a.category != b.category) return a.category < b.category;
return a.label < b.label;
});
m_shaderNodeCacheBuilt = true;
return m_shaderNodeCache;
}
std::vector<pxr::SdfPath> MaterialManager::GetAllMaterials() const {
std::vector<pxr::SdfPath> result;
if (!m_stage) return result;
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
if (prim.IsA<pxr::UsdShadeMaterial>())
result.push_back(prim.GetPath());
}
return result;
}
ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& materialPath) const {
ShaderGraphSnapshot snapshot;
if (!m_stage) return snapshot;
pxr::UsdPrim materialPrim = m_stage->GetPrimAtPath(materialPath);
if (!materialPrim.IsValid()) return snapshot;
static const pxr::TfToken kUiPositionKey("uiPosition");
static const pxr::TfToken kUiPinDisplayModeKey("uiPinDisplayMode");
// Seed with every shader under the material, recursing into nested node
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
// scopes rather than authoring them as direct children).
std::vector<pxr::UsdPrim> pending;
std::vector<pxr::UsdPrim> scopes{materialPrim};
while (!scopes.empty()) {
pxr::UsdPrim scope = scopes.back();
scopes.pop_back();
for (const pxr::UsdPrim& child : scope.GetChildren()) {
if (child.IsA<pxr::UsdShadeShader>())
pending.push_back(child);
else if (child.IsA<pxr::UsdShadeNodeGraph>())
scopes.push_back(child);
}
}
// Worklist walk: resolving each input's true upstream shader output pulls
// in nodes living in node graphs outside the material prim too.
std::set<pxr::SdfPath> visited;
while (!pending.empty()) {
pxr::UsdPrim child = pending.back();
pending.pop_back();
if (!visited.insert(child.GetPath()).second)
continue;
pxr::UsdShadeShader shader(child);
if (!shader) continue;
ShaderGraphNode node;
node.path = child.GetPath();
pxr::TfToken shaderId;
shader.GetIdAttr().Get(&shaderId);
node.shaderId = shaderId.GetString();
pxr::VtValue posValue = child.GetCustomDataByKey(kUiPositionKey);
if (posValue.IsHolding<pxr::GfVec2f>()) {
node.uiPosition = posValue.UncheckedGet<pxr::GfVec2f>();
node.hasAuthoredPosition = true;
}
pxr::VtValue pinModeValue = child.GetCustomDataByKey(kUiPinDisplayModeKey);
if (pinModeValue.IsHolding<int>()) {
int raw = pinModeValue.UncheckedGet<int>();
if (raw >= static_cast<int>(PinDisplayMode::All) && raw <= static_cast<int>(PinDisplayMode::Collapsed))
node.pinDisplayMode = static_cast<PinDisplayMode>(raw);
}
// Prefer the full Sdr-defined pin set (so unauthored pins can still be
// dragged to create a connection); fall back to authored attributes
// only for shader types the registry doesn't know about.
pxr::SdrShaderNodeConstPtr sdrNode =
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(shaderId);
if (sdrNode) {
node.category = DeriveCategory(sdrNode);
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
if (auto* prop = sdrNode->GetShaderInput(inputName)) {
ShaderPinInfo pin{inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()};
for (const auto& option : prop->GetOptions())
pin.allowedValues.push_back(option.first.GetString());
node.inputs.push_back(std::move(pin));
}
}
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
if (auto* prop = sdrNode->GetShaderOutput(outputName))
node.outputs.push_back({outputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
}
} else {
for (const auto& input : shader.GetInputs())
node.inputs.push_back({input.GetBaseName().GetString(), input.GetTypeName()});
for (const auto& output : shader.GetOutputs())
node.outputs.push_back({output.GetBaseName().GetString(), output.GetTypeName()});
}
// Only authored inputs can carry a connection. Resolve each one
// through node-graph boundaries (shaderOutputsOnly) so a MaterialX
// connection routed via NodeGraph interface attrs links straight to
// the shader output that actually produces the value.
for (const auto& input : shader.GetInputs()) {
for (const pxr::UsdAttribute& sourceAttr :
pxr::UsdShadeUtils::GetValueProducingAttributes(input, /*shaderOutputsOnly=*/true)) {
pxr::UsdPrim sourcePrim = sourceAttr.GetPrim();
if (!sourcePrim.IsA<pxr::UsdShadeShader>()) continue;
ShaderGraphLink link;
link.destNode = child.GetPath();
link.destInput = input.GetBaseName().GetString();
link.sourceNode = sourcePrim.GetPath();
link.sourceOutput = pxr::UsdShadeOutput(sourceAttr).GetBaseName().GetString();
snapshot.links.push_back(std::move(link));
pending.push_back(sourcePrim); // may live outside the material
}
}
snapshot.nodes.push_back(std::move(node));
}
return snapshot;
}
} // namespace UsdLayerManager
+115
View File
@@ -0,0 +1,115 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/base/gf/vec2f.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
/// One entry in the shader-node creation menu, derived from the Sdr/Ndr
/// shader registry (covers UsdPreviewSurface, MaterialX, Arnold, Cycles,
/// etc. — whatever shader definitions are discoverable in this build).
struct ShaderNodeTypeInfo {
std::string identifier; ///< Sdr identifier, authored as info:id
std::string label; ///< display label for the create-node menu
std::string family; ///< Sdr family (parser-provided, may be empty)
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
/// "Utility", "Light") derived from Sdr context/role/output types; the
/// create-node list groups by it. Never empty.
std::string category;
/// Top-level create-list group derived from the Sdr source type:
/// "USD" (glslfx/OSL core nodes), "MaterialX", "Arnold", "Cycles", or a
/// capitalized raw source type. Never empty.
std::string source;
};
/// One input or output pin on a shader node, with its USD value type so
/// connections/CreateInput/CreateOutput can author the right type.
struct ShaderPinInfo {
std::string name;
pxr::SdfValueTypeName typeName;
/// Sdr-declared allowed values (e.g. UsdUVTexture's sourceColorSpace:
/// "raw"/"sRGB"/"auto"), empty when the property isn't enum-constrained.
/// Drives a dropdown instead of a free-text field in the property panel.
std::vector<std::string> allowedValues;
};
/// Hypershade-style pin display mode for a node in the graph canvas.
/// All: every Sdr-defined pin shown (today's default). Connected: only
/// linked pins shown, the rest collapsed behind a single connectable nub per
/// side. Collapsed: every pin — linked or not — collapses behind that one
/// nub per side, so existing connections visually converge on it too.
enum class PinDisplayMode {
All = 0,
Connected = 1,
Collapsed = 2,
};
/// One UsdShadeShader prim read back from an existing material network.
/// inputs/outputs list the full Sdr-defined pin set (not just authored
/// attributes) so unauthored pins can still be dragged to create a connection.
struct ShaderGraphNode {
pxr::SdfPath path;
std::string shaderId;
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
/// "Utility", "Light") from the same classifier the create-node list uses;
/// empty when the shader id isn't in the Sdr registry. Drives which nodes
/// get an in-canvas thumbnail.
std::string category;
pxr::GfVec2f uiPosition{0.0f, 0.0f};
/// False when no uiPosition custom data is authored (typical for networks
/// referenced from .mtlx) — the editor auto-lays such nodes out instead.
bool hasAuthoredPosition = false;
/// Pin display mode, read from customData when the "keep graph node view
/// settings in USD" preference authored it; All when unauthored.
PinDisplayMode pinDisplayMode = PinDisplayMode::All;
std::vector<ShaderPinInfo> inputs;
std::vector<ShaderPinInfo> outputs;
};
/// One authored connection between two shader nodes in the same network.
struct ShaderGraphLink {
pxr::SdfPath destNode;
std::string destInput;
pxr::SdfPath sourceNode;
std::string sourceOutput;
};
struct ShaderGraphSnapshot {
std::vector<ShaderGraphNode> nodes;
std::vector<ShaderGraphLink> links;
};
/// Reads/introspects UsdShade material networks and the Sdr shader registry.
/// Mirrors PropertyManager's role: holds read/derive logic only — mutations
/// go through ICommand subclasses in src/core/commands/.
class MaterialManager {
public:
void SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; }
/// All shader node types registered in the Sdr registry, grouped for a
/// categorized create-node menu. Cached after the first (slow) call.
const std::vector<ShaderNodeTypeInfo>& GetAvailableShaderNodes();
/// The entire shader network of the material at materialPath: shaders
/// under the material (recursing into nested UsdShadeNodeGraphs) plus
/// everything reachable upstream through connections — including nodes in
/// node graphs outside the material, as authored by MaterialX (.mtlx)
/// imports. Connections are resolved through node-graph boundary attrs to
/// the shader outputs that actually produce the values.
ShaderGraphSnapshot GetShaderGraph(const pxr::SdfPath& materialPath) const;
/// Paths of every UsdShadeMaterial prim on the stage, in traversal order.
/// Drives the material browser list (Hypershade-style).
std::vector<pxr::SdfPath> GetAllMaterials() const;
private:
pxr::UsdStageRefPtr m_stage;
std::vector<ShaderNodeTypeInfo> m_shaderNodeCache;
bool m_shaderNodeCacheBuilt = false;
};
} // namespace UsdLayerManager
+456
View File
@@ -0,0 +1,456 @@
#include "RenderLayerManager.h"
#include "LayerManager.h"
#include "../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usd/editTarget.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/sdf/primSpec.h>
#include <pxr/usd/sdf/relationshipSpec.h>
#include <pxr/usd/sdf/proxyTypes.h>
#include <pxr/usd/usdGeom/imageable.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <functional>
#include <set>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
namespace {
// Reserved, undefined ("over"-only) admin prim path each render layer's own
// SdfLayer carries its membership relationship on. Never a `def`, so it never
// shows up in UsdStage::Traverse() / the Scene Hierarchy panel / GetChildren().
const SdfPath kAdminPrimPath("/RenderLayerData");
const TfToken kMembersIncludesName("collection:members:includes");
// SdfLayer custom-layer-data keys.
const std::string kMarkerKey = "usdLayerManager:renderLayer";
const std::string kNameKey = "usdLayerManager:renderLayerName";
const std::string kAutoHiddenKey = "usdLayerManager:autoHiddenPaths";
// Root-layer-only key: persists which render layer was active across save/reload.
const std::string kActiveLayerKey = "usdLayerManager:activeRenderLayer";
std::string SanitizeFileStem(const std::string& raw) {
std::string result;
result.reserve(raw.size());
for (char c : raw) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-')
result += c;
else
result += '_';
}
return result;
}
SdfPath MembersIncludesPath() {
return kAdminPrimPath.AppendProperty(kMembersIncludesName);
}
} // namespace
RenderLayerManager::RenderLayerManager() {}
RenderLayerManager::~RenderLayerManager() {}
void RenderLayerManager::SetStage(UsdStageRefPtr stage) {
m_stage = stage;
Refresh();
}
void RenderLayerManager::Refresh() {
BuildList();
}
void RenderLayerManager::BuildList() {
m_layers.clear();
m_layerRefs.clear();
RenderLayerInfo def;
def.name = "Default";
def.isDefault = true;
m_layers.push_back(def);
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
bool anyActive = false;
for (const auto& subPath : rootLayer->GetSubLayerPaths()) {
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath);
if (!sub) continue;
VtDictionary cld = sub->GetCustomLayerData();
auto markerIt = cld.find(kMarkerKey);
if (markerIt == cld.end() || !markerIt->second.IsHolding<bool>() ||
!markerIt->second.Get<bool>())
continue; // not a render layer sublayer — leave for LayerManager/StageEditorPanel
m_layerRefs.push_back(sub);
RenderLayerInfo info;
info.layer = SdfLayerHandle(sub);
info.layerIdentifier = sub->GetIdentifier();
auto nameIt = cld.find(kNameKey);
info.name = (nameIt != cld.end() && nameIt->second.IsHolding<std::string>())
? nameIt->second.Get<std::string>()
: LayerManager::ExtractDisplayName(info.layerIdentifier);
info.isActive = !m_stage->IsLayerMuted(info.layerIdentifier);
if (info.isActive) anyActive = true;
m_layers.push_back(info);
}
m_layers[0].isActive = !anyActive;
}
/*static*/ bool RenderLayerManager::IsRenderLayerSublayer(const SdfLayerHandle& layer) {
if (!layer) return false;
VtDictionary cld = layer->GetCustomLayerData();
auto it = cld.find(kMarkerKey);
return it != cld.end() && it->second.IsHolding<bool>() && it->second.Get<bool>();
}
void RenderLayerManager::RefreshLayerManager() {
if (m_layerManager) m_layerManager->Refresh();
}
SdfLayerHandle RenderLayerManager::FindRenderLayer(const std::string& layerId) const {
for (const auto& info : m_layers)
if (!info.isDefault && info.layerIdentifier == layerId)
return info.layer;
return SdfLayerHandle();
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
std::string RenderLayerManager::CreateRenderLayer(const std::string& name) {
if (!m_stage) return {};
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return {};
namespace fs = std::filesystem;
fs::path baseDir;
std::string realPath = rootLayer->GetRealPath();
baseDir = !realPath.empty()
? fs::path(realPath).parent_path() / ".renderlayers"
: fs::current_path() / ".renderlayers";
std::error_code ec;
fs::create_directories(baseDir, ec);
std::string stem = SanitizeFileStem(name.empty() ? "RenderLayer" : name);
if (stem.empty()) stem = "RenderLayer";
std::string finalStem = stem;
fs::path filePath = baseDir / (finalStem + ".usda");
int suffix = 1;
while (fs::exists(filePath)) {
finalStem = stem + "_" + std::to_string(suffix++);
filePath = baseDir / (finalStem + ".usda");
}
SdfLayerRefPtr newLayer = SdfLayer::CreateNew(filePath.string());
if (!newLayer) {
LOG_ERROR("RenderLayerManager: failed to create layer file: " + filePath.string());
return {};
}
VtDictionary cld;
cld[kMarkerKey] = VtValue(true);
cld[kNameKey] = VtValue(name.empty() ? finalStem : name);
newLayer->SetCustomLayerData(cld);
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(newLayer, kAdminPrimPath);
if (adminPrim)
SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
/*custom=*/false, SdfVariabilityUniform);
newLayer->Save();
rootLayer->InsertSubLayerPath(filePath.string(), 0);
m_stage->MuteLayer(filePath.string()); // starts inactive until explicitly activated
Refresh();
RefreshLayerManager();
return newLayer->GetIdentifier();
}
bool RenderLayerManager::DeleteRenderLayer(const std::string& layerId) {
if (!m_stage) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
if (!FindRenderLayer(layerId)) return false;
if (GetActiveRenderLayerId() == layerId)
ActivateDefaultLayer();
SdfSubLayerProxy subPaths = rootLayer->GetSubLayerPaths();
for (size_t i = 0; i < subPaths.size(); ++i) {
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPaths[i]);
if (sub && sub->GetIdentifier() == layerId) {
subPaths.erase(subPaths.begin() + static_cast<long>(i));
Refresh();
RefreshLayerManager();
return true;
}
}
return false;
}
bool RenderLayerManager::RestoreDeletedRenderLayer(const std::string& layerIdentifier) {
if (!m_stage) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
rootLayer->InsertSubLayerPath(layerIdentifier, 0);
Refresh();
RefreshLayerManager();
return true;
}
bool RenderLayerManager::RenameRenderLayer(const std::string& layerId, const std::string& newName) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer || newName.empty()) return false;
VtDictionary cld = layer->GetCustomLayerData();
cld[kNameKey] = VtValue(newName);
layer->SetCustomLayerData(cld);
Refresh();
RefreshLayerManager();
return true;
}
// ---------------------------------------------------------------------------
// Switching
// ---------------------------------------------------------------------------
bool RenderLayerManager::ActivateRenderLayer(const std::string& layerId) {
if (!m_stage) return false;
SdfLayerHandle target = FindRenderLayer(layerId);
if (!target) return false;
// First entry into render-layer mode this "session" (Default -> a render
// layer): remember the edit target so ActivateDefaultLayer() can restore
// it later, no matter how many render-layer-to-render-layer switches
// happen in between.
if (IsDefaultActive()) {
SdfLayerHandle currentEt = m_stage->GetEditTarget().GetLayer();
m_savedEditTargetIdBeforeMode = currentEt ? currentEt->GetIdentifier() : std::string();
}
for (const auto& info : m_layers) {
if (info.isDefault || info.layerIdentifier == layerId) continue;
if (!m_stage->IsLayerMuted(info.layerIdentifier))
m_stage->MuteLayer(info.layerIdentifier);
}
if (m_stage->IsLayerMuted(layerId))
m_stage->UnmuteLayer(layerId);
if (m_layerManager) m_layerManager->SetEditTarget(layerId);
else m_stage->SetEditTarget(UsdEditTarget(target));
RecomputeIsolation(target, layerId);
PersistActiveLayerId(layerId);
Refresh();
RefreshLayerManager();
return true;
}
void RenderLayerManager::ActivateDefaultLayer() {
if (!m_stage) return;
for (const auto& info : m_layers) {
if (info.isDefault) continue;
if (!m_stage->IsLayerMuted(info.layerIdentifier))
m_stage->MuteLayer(info.layerIdentifier);
}
if (!m_savedEditTargetIdBeforeMode.empty()) {
if (m_layerManager) {
m_layerManager->SetEditTarget(m_savedEditTargetIdBeforeMode);
} else if (SdfLayerHandle saved = SdfLayer::Find(m_savedEditTargetIdBeforeMode)) {
m_stage->SetEditTarget(UsdEditTarget(saved));
}
}
m_savedEditTargetIdBeforeMode.clear();
PersistActiveLayerId("");
Refresh();
RefreshLayerManager();
}
std::string RenderLayerManager::GetActiveRenderLayerId() const {
for (const auto& info : m_layers)
if (!info.isDefault && info.isActive)
return info.layerIdentifier;
return {};
}
void RenderLayerManager::PersistActiveLayerId(const std::string& layerId) {
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
VtDictionary cld = rootLayer->GetCustomLayerData();
if (layerId.empty()) cld.erase(kActiveLayerKey);
else cld[kActiveLayerKey] = VtValue(layerId);
rootLayer->SetCustomLayerData(cld);
}
void RenderLayerManager::RestorePersistedActiveLayer() {
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
VtDictionary cld = rootLayer->GetCustomLayerData();
auto it = cld.find(kActiveLayerKey);
if (it == cld.end() || !it->second.IsHolding<std::string>()) return;
std::string layerId = it->second.Get<std::string>();
if (FindRenderLayer(layerId))
ActivateRenderLayer(layerId);
}
// ---------------------------------------------------------------------------
// Membership (Sdf-level — works regardless of mute state)
// ---------------------------------------------------------------------------
std::vector<SdfPath> RenderLayerManager::GetMembers(const std::string& layerId) const {
std::vector<SdfPath> result;
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return result;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) return result;
for (const SdfPath& p : rel->GetTargetPathList().GetAddedOrExplicitItems())
result.push_back(p);
return result;
}
bool RenderLayerManager::AddMember(const std::string& layerId, const SdfPath& path) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return false;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) {
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(layer, kAdminPrimPath);
if (!adminPrim) return false;
rel = SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
/*custom=*/false, SdfVariabilityUniform);
}
if (!rel) return false;
// Add() doesn't de-duplicate against existing explicit items — guard
// here so repeated adds of the same path (e.g. re-running a command, or
// a redundant drag) don't pile up duplicate targets.
auto existing = rel->GetTargetPathList().GetAddedOrExplicitItems();
if (std::find(existing.begin(), existing.end(), path) == existing.end())
rel->GetTargetPathList().Add(path);
if (GetActiveRenderLayerId() == layerId)
RecomputeIsolation(layer, layerId);
RefreshLayerManager(); // keeps StageEditorPanel's dirty-flag asterisk current
return true;
}
bool RenderLayerManager::RemoveMember(const std::string& layerId, const SdfPath& path) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return false;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) return false;
rel->GetTargetPathList().Remove(path);
if (GetActiveRenderLayerId() == layerId)
RecomputeIsolation(layer, layerId);
RefreshLayerManager();
return true;
}
// ---------------------------------------------------------------------------
// Viewport isolation
// ---------------------------------------------------------------------------
void RenderLayerManager::ClearAutoHiddenPaths(const SdfLayerHandle& layer) {
if (!layer) return;
VtDictionary cld = layer->GetCustomLayerData();
auto it = cld.find(kAutoHiddenKey);
if (it == cld.end()) return;
if (it->second.IsHolding<VtArray<std::string>>()) {
for (const std::string& p : it->second.Get<VtArray<std::string>>()) {
SdfPath primPath(p);
SdfPrimSpecHandle owner = layer->GetPrimAtPath(primPath);
SdfAttributeSpecHandle attrSpec =
layer->GetAttributeAtPath(primPath.AppendProperty(TfToken("visibility")));
if (owner && attrSpec)
owner->RemoveProperty(attrSpec);
}
}
cld.erase(it);
layer->SetCustomLayerData(cld);
}
void RenderLayerManager::RecomputeIsolation(const SdfLayerHandle& layer, const std::string& layerId) {
if (!m_stage || !layer) return;
ClearAutoHiddenPaths(layer);
std::vector<SdfPath> memberList = GetMembers(layerId);
std::set<SdfPath> members(memberList.begin(), memberList.end());
// Every strict prefix of every member path: a subtree rooted at one of
// these paths might still contain a member somewhere below it, so it
// must be recursed into rather than hidden outright.
std::set<SdfPath> ancestors;
for (const SdfPath& m : memberList) {
SdfPath p = m.GetParentPath();
while (!p.IsEmpty()) {
if (!ancestors.insert(p).second) break; // already inserted by another member
if (p == SdfPath::AbsoluteRootPath()) break;
p = p.GetParentPath();
}
}
std::vector<std::string> newHidden;
std::function<void(const UsdPrim&)> visit = [&](const UsdPrim& prim) {
for (const UsdPrim& child : prim.GetChildren()) {
SdfPath cp = child.GetPath();
if (cp == kAdminPrimPath) continue;
if (members.count(cp)) {
// Exact member: whole subtree stays default-visible (expandPrims
// semantics — everything under a member is implicitly included),
// nothing to author, no need to recurse.
continue;
}
if (ancestors.count(cp)) {
visit(child); // a member lies somewhere below — keep exploring
continue;
}
UsdGeomImageable img(child);
if (img) {
UsdEditContext ec(m_stage, layer);
img.GetVisibilityAttr().Set(UsdGeomTokens->invisible);
newHidden.push_back(cp.GetString());
// Minimal cut: stop here, children inherit the hidden state.
} else {
// Can't author visibility on a non-imageable prim (e.g. a pure
// Scope holding unrelated data) — look deeper for an imageable
// descendant to hide instead, rather than silently failing to
// isolate this whole branch.
visit(child);
}
}
};
visit(m_stage->GetPseudoRoot());
if (!newHidden.empty()) {
VtDictionary cld = layer->GetCustomLayerData();
cld[kAutoHiddenKey] = VtValue(VtArray<std::string>(newHidden.begin(), newHidden.end()));
layer->SetCustomLayerData(cld);
}
}
} // namespace UsdLayerManager
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/path.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
class LayerManager;
/// One row for the Render Layer panel. The "Default" entry (isDefault=true)
/// has no backing layer — it represents the unmodified base stage, i.e. every
/// render-layer sublayer muted.
struct RenderLayerInfo {
std::string name;
pxr::SdfLayerHandle layer; ///< empty for Default
std::string layerIdentifier; ///< empty for Default; the stable key used everywhere below
bool isActive = false;
bool isDefault = false;
};
/// Maya-Render-Layers-style layers built on the stage's own sublayer stack.
///
/// Each render layer is a dedicated, file-backed SdfLayer inserted as a
/// sublayer of the stage's root layer (tagged via SdfLayer custom layer data
/// so it's distinguishable from ordinary content sublayers). At most one is
/// ever unmuted ("active") at a time; activating one mutes the rest and
/// repoints the stage's ambient edit target at it, so every existing
/// attribute/transform/shader-connection/material-binding command in the app
/// already lands its edits in the right place with no changes of its own
/// (see docs/adr/0002-render-layer.md).
///
/// Membership (which lights/objects belong to a layer) is stored as a
/// UsdCollectionAPI-compatible relationship (`collection:members:includes`)
/// on a reserved, undefined ("over"-only, never rendered) admin prim inside
/// that layer's own SdfLayer. All reads/writes of it go through raw Sdf-level
/// API rather than UsdCollectionAPI/UsdStage, because muted layers are fully
/// excluded from stage composition — GetPrimAtPath would fail for every
/// layer except the currently-active one, and the panel needs to edit
/// membership on layers that aren't active.
///
/// Switching layers also isolates the viewport to that layer's members: a
/// single top-down traversal authors the minimal set of
/// `visibility = invisible` opinions (into the active layer's own SdfLayer)
/// needed to hide every subtree that contains no member, tracked via that
/// layer's custom layer data so it can be precisely cleared and recomputed
/// without disturbing deliberate visibility overrides.
class RenderLayerManager {
public:
RenderLayerManager();
~RenderLayerManager();
void SetStage(pxr::UsdStageRefPtr stage);
void SetLayerManager(LayerManager* mgr) { m_layerManager = mgr; }
void Refresh();
/// Default first, then user-created render layers in sublayer order.
std::vector<RenderLayerInfo> GetRenderLayers() const { return m_layers; }
/// Creates a new file-backed render layer next to the stage's root layer
/// (under a ".renderlayers" subdirectory), muted, with an empty member
/// list. Returns the new layer's identifier, or empty on failure.
std::string CreateRenderLayer(const std::string& name);
bool DeleteRenderLayer(const std::string& layerId);
/// Re-inserts a render layer previously removed by DeleteRenderLayer — its
/// backing file is left on disk untouched by DeleteRenderLayer, so this
/// just restores the sublayer reference (always at the top of the stack,
/// same as a newly-created layer). Used by RenderLayerDeleteCommand::Undo().
bool RestoreDeletedRenderLayer(const std::string& layerIdentifier);
bool RenameRenderLayer(const std::string& layerId, const std::string& newName);
/// Mutes every other render-layer sublayer, unmutes layerId, repoints the
/// stage's ambient edit target at it, and recomputes viewport isolation.
/// The first time this is called while Default is active, it remembers
/// the current edit target so ActivateDefaultLayer() can restore it later
/// — callers (e.g. an undoable switch command) don't need to manage that
/// themselves; it's tracked here so it stays correct across Undo/Redo.
bool ActivateRenderLayer(const std::string& layerId);
/// Mutes every render-layer sublayer (back to the unmodified base stage)
/// and restores whatever the edit target was before render-layer mode
/// was first entered (see ActivateRenderLayer's note).
void ActivateDefaultLayer();
std::string GetActiveRenderLayerId() const;
bool IsDefaultActive() const { return GetActiveRenderLayerId().empty(); }
/// Sdf-level: the raw, unexpanded `includes` target list. Works
/// regardless of whether layerId is currently muted.
std::vector<pxr::SdfPath> GetMembers(const std::string& layerId) const;
bool AddMember(const std::string& layerId, const pxr::SdfPath& path);
bool RemoveMember(const std::string& layerId, const pxr::SdfPath& path);
/// Re-applies whichever render layer was active the last time this stage
/// was saved (persisted as root-layer custom layer data, since USD mute
/// state itself is a runtime flag and isn't serialized). Call once after
/// SetStage(). No-op if nothing was persisted, or the persisted layer no
/// longer exists.
void RestorePersistedActiveLayer();
/// True if this layer carries the "this sublayer is a render layer"
/// marker RenderLayerManager itself authors on creation. Exposed so
/// other panels (e.g. StageEditorPanel) that walk the raw sublayer list
/// can recognize and guard-rail render-layer sublayers without
/// duplicating the custom-layer-data key.
static bool IsRenderLayerSublayer(const pxr::SdfLayerHandle& layer);
private:
void BuildList();
// LayerManager keeps its own cached sublayer/mute-state snapshot,
// rebuilt only on its own SetStage()/Refresh()/mutator calls — it has no
// way to know when RenderLayerManager mutates the root layer's sublayer
// stack or a layer's mute state directly (bypassing LayerManager's own
// methods, which every RenderLayerManager mutator below does). Called
// after every such mutation so StageEditorPanel (backed by LayerManager)
// doesn't show a stale sublayer list.
void RefreshLayerManager();
pxr::SdfLayerHandle FindRenderLayer(const std::string& layerId) const;
void RecomputeIsolation(const pxr::SdfLayerHandle& layer, const std::string& layerId);
void ClearAutoHiddenPaths(const pxr::SdfLayerHandle& layer);
void PersistActiveLayerId(const std::string& layerId);
pxr::UsdStageRefPtr m_stage;
LayerManager* m_layerManager = nullptr;
std::vector<RenderLayerInfo> m_layers; // [0] is always the Default entry
// Strong refs so muted render-layer SdfLayers stay alive between
// Refresh() calls (mirrors LayerManager::m_layerRefs).
std::vector<pxr::SdfLayerRefPtr> m_layerRefs;
// Edit target identifier captured the moment render-layer mode is first
// entered (Default -> any render layer); restored and cleared by
// ActivateDefaultLayer(). Empty when not currently "in mode".
std::string m_savedEditTargetIdBeforeMode;
};
} // namespace UsdLayerManager
+472 -21
View File
@@ -8,6 +8,14 @@
#include <pxr/usd/usdGeom/tokens.h> #include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/bboxCache.h> #include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/camera.h> #include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdLux/lightAPI.h>
#include <pxr/usd/usdLux/sphereLight.h>
#include <pxr/usd/usdLux/rectLight.h>
#include <pxr/usd/usdLux/diskLight.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/cylinderLight.h>
#include <pxr/imaging/glf/contextCaps.h> #include <pxr/imaging/glf/contextCaps.h>
#include <pxr/imaging/cameraUtil/conformWindow.h> #include <pxr/imaging/cameraUtil/conformWindow.h>
#include <pxr/imaging/cameraUtil/framing.h> #include <pxr/imaging/cameraUtil/framing.h>
@@ -15,8 +23,15 @@
#include <pxr/base/gf/rect2i.h> #include <pxr/base/gf/rect2i.h>
#include <pxr/base/gf/frustum.h> #include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h> #include <pxr/base/gf/rotation.h>
#include <pxr/base/tf/diagnosticMgr.h>
#include <pxr/base/tf/error.h>
#include <pxr/base/tf/warning.h>
#include <pxr/base/tf/status.h>
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdlib>
#include <exception>
#include <unordered_set> #include <unordered_set>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -30,6 +45,37 @@ static std::string Vec3fStr(const pxr::GfVec3f& v) {
+ std::to_string(v[2]) + ")"; + std::to_string(v[2]) + ")";
} }
// Forwards USD's TfError / TF_WARN / TF_STATUS messages into the app log.
// Without this, Hydra diagnostics (e.g. the reason HdxColorCorrectionTask
// fails to build an OCIO processor) only go to a stderr console the GUI
// never shows. Installed once, process-wide, from InitRenderer().
namespace {
class UsdDiagnosticLogger : public pxr::TfDiagnosticMgr::Delegate {
public:
void IssueError(pxr::TfError const& err) override {
LOG_ERROR("[USD] " + err.GetCommentary());
}
void IssueFatalError(pxr::TfCallContext const&,
std::string const& msg) override {
LOG_ERROR("[USD fatal] " + msg);
}
void IssueStatus(pxr::TfStatus const& status) override {
LOG_INFO("[USD] " + status.GetCommentary());
}
void IssueWarning(pxr::TfWarning const& warning) override {
LOG_WARNING("[USD] " + warning.GetCommentary());
}
};
void InstallUsdDiagnosticLogger() {
static UsdDiagnosticLogger s_logger; // process-lifetime; never removed
static bool s_installed = false;
if (s_installed) return;
s_installed = true;
pxr::TfDiagnosticMgr::GetInstance().AddDelegate(&s_logger);
}
} // namespace
/// Port of stageView._ComputeCameraFraming(): /// Port of stageView._ComputeCameraFraming():
/// Converts a Y-up integer viewport rect into a CameraUtilFraming whose /// Converts a Y-up integer viewport rect into a CameraUtilFraming whose
/// display/data windows are expressed in the Y-down coordinate system that /// display/data windows are expressed in the Y-down coordinate system that
@@ -120,8 +166,16 @@ void main() { outColor = color; }
UsdSceneRenderer::UsdSceneRenderer() UsdSceneRenderer::UsdSceneRenderer()
: m_showGrid(true) : m_showGrid(true)
, m_showCameraGuide(false)
, m_showGuides(false)
, m_showProxy(true)
, m_showRender(false)
, m_currentAov(pxr::TfToken("color"))
, m_aaEnabled(false) , m_aaEnabled(false)
, m_backgroundColor(0.15f, 0.15f, 0.15f) , m_backgroundColor(0.15f, 0.15f, 0.15f)
, m_shadingMode(ShadingMode::SmoothShaded)
, m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
, m_ambientLightOnly(true) , m_ambientLightOnly(true)
, m_domeLightEnabled(false) , m_domeLightEnabled(false)
, m_stageIsZup(false) , m_stageIsZup(false)
@@ -160,6 +214,7 @@ UsdSceneRenderer::~UsdSceneRenderer() {
DestroyAxisResources(); DestroyAxisResources();
DestroyBBoxResources(); DestroyBBoxResources();
DestroyCamWireResources(); DestroyCamWireResources();
DestroyLightWireResources();
} }
// =========================================================================== // ===========================================================================
@@ -177,6 +232,7 @@ void UsdSceneRenderer::SetStage(pxr::UsdStageRefPtr stage) {
DestroyAxisResources(); DestroyAxisResources();
DestroyBBoxResources(); DestroyBBoxResources();
DestroyCamWireResources(); DestroyCamWireResources();
DestroyLightWireResources();
// Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup) // Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup)
if (stage) { if (stage) {
@@ -199,6 +255,9 @@ void UsdSceneRenderer::InitRenderer() {
LOG_INFO("UsdSceneRenderer::InitRenderer - initializing..."); LOG_INFO("UsdSceneRenderer::InitRenderer - initializing...");
// Route USD/Hydra diagnostics (incl. OCIO failures) to the app log.
InstallUsdDiagnosticLogger();
pxr::GlfContextCaps::InitInstance(); pxr::GlfContextCaps::InitInstance();
pxr::UsdImagingGLEngine::Parameters params; pxr::UsdImagingGLEngine::Parameters params;
@@ -213,31 +272,37 @@ void UsdSceneRenderer::InitRenderer() {
// Plugin selection: prefer HdStorm / GL-based renderers // Plugin selection: prefer HdStorm / GL-based renderers
auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins(); auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins();
LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size())); LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size()));
bool hasCycles = false;
for (const auto& p : plugins) { for (const auto& p : plugins) {
LOG_INFO(" " + std::string(p.GetText()) + " -> " LOG_INFO(" " + std::string(p.GetText()) + " -> "
+ pxr::UsdImagingGLEngine::GetRendererDisplayName(p)); + pxr::UsdImagingGLEngine::GetRendererDisplayName(p));
if (std::string(p.GetText()) == "HdCyclesPlugin") hasCycles = true;
} }
if (!hasCycles)
LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors).");
pxr::TfToken currentPlugin = m_renderer->GetCurrentRendererId(); // Apply the requested plugin. m_currentRendererPlugin is set from global
if (currentPlugin.IsEmpty() && !plugins.empty()) { // preferences before InitRenderer runs; always honour it rather than
pxr::TfToken best; // checking whether the engine already chose something by default.
// If a plugin was previously selected, honour it
if (!m_currentRendererPlugin.IsEmpty()) { if (!m_currentRendererPlugin.IsEmpty()) {
best = m_currentRendererPlugin; pxr::TfToken cur = m_renderer->GetCurrentRendererId();
} else { if (cur != m_currentRendererPlugin)
m_renderer->SetRendererPlugin(m_currentRendererPlugin);
} else if (!plugins.empty()) {
// Fallback heuristic: prefer Storm/GL
pxr::TfToken best;
for (const auto& p : plugins) { for (const auto& p : plugins) {
std::string n(p.GetText()); std::string n(p.GetText());
if (n.find("Storm") != std::string::npos || if (n.find("Storm") != std::string::npos ||
n.find("GL") != std::string::npos) { best = p; break; } n.find("GL") != std::string::npos) { best = p; break; }
} }
if (best.IsEmpty()) best = plugins[0]; if (best.IsEmpty()) best = plugins[0];
}
m_renderer->SetRendererPlugin(best); m_renderer->SetRendererPlugin(best);
LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));
} }
LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));
// Enable AOV "color" (matches stageView._handleRendererChanged) // Enable AOV (matches stageView._handleRendererChanged; uses saved m_currentAov)
m_renderer->SetRendererAov(pxr::TfToken("color")); m_renderer->SetRendererAov(m_currentAov);
// Selection highlight color (usdview default: yellow) // Selection highlight color (usdview default: yellow)
m_renderer->SetSelectionColor(pxr::GfVec4f(1.0f, 1.0f, 0.0f, 1.0f)); m_renderer->SetSelectionColor(pxr::GfVec4f(1.0f, 1.0f, 0.0f, 1.0f));
@@ -252,6 +317,7 @@ void UsdSceneRenderer::InitRenderer() {
InitAxisResources(); InitAxisResources();
InitBBoxResources(); InitBBoxResources();
InitCamWireResources(); InitCamWireResources();
InitLightWireResources();
} }
// =========================================================================== // ===========================================================================
@@ -281,8 +347,9 @@ bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
bool ok = m_renderer->SetRendererPlugin(pluginId); bool ok = m_renderer->SetRendererPlugin(pluginId);
if (ok) { if (ok) {
// Re-enable colour AOV after delegate switch (mirrors usdtweak behaviour) // Reset AOV to "color" on every delegate switch (mirrors stageView._handleRendererChanged)
m_renderer->SetRendererAov(pxr::TfToken("color")); m_currentAov = pxr::TfToken("color");
m_renderer->SetRendererAov(m_currentAov);
LOG_INFO("Render delegate switched to: " + std::string(pluginId.GetText())); LOG_INFO("Render delegate switched to: " + std::string(pluginId.GetText()));
} else { } else {
LOG_ERROR("Failed to switch render delegate to: " + std::string(pluginId.GetText())); LOG_ERROR("Failed to switch render delegate to: " + std::string(pluginId.GetText()));
@@ -290,6 +357,39 @@ bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
return ok; return ok;
} }
pxr::TfTokenVector UsdSceneRenderer::GetRendererAovs() const
{
if (!m_renderer) return {};
return m_renderer->GetRendererAovs();
}
bool UsdSceneRenderer::SetCurrentAov(const pxr::TfToken& aov)
{
if (!m_renderer) return false;
if (m_renderer->SetRendererAov(aov)) {
m_currentAov = aov;
return true;
}
return false;
}
pxr::UsdImagingGLRendererSettingsList UsdSceneRenderer::GetRendererSettingsList() const
{
if (!m_renderer) return {};
return m_renderer->GetRendererSettingsList();
}
pxr::VtValue UsdSceneRenderer::GetRendererSetting(const pxr::TfToken& id) const
{
if (!m_renderer) return {};
return m_renderer->GetRendererSetting(id);
}
void UsdSceneRenderer::SetRendererSetting(const pxr::TfToken& id, const pxr::VtValue& v)
{
if (m_renderer) m_renderer->SetRendererSetting(id, v);
}
// =========================================================================== // ===========================================================================
// Camera State // Camera State
// =========================================================================== // ===========================================================================
@@ -485,6 +585,8 @@ bool UsdSceneRenderer::PickObjectsInRect(
void UsdSceneRenderer::Render(int width, int height) void UsdSceneRenderer::Render(int width, int height)
{ {
if (!m_stage || width <= 0 || height <= 0) return; if (!m_stage || width <= 0 || height <= 0) return;
m_lastRenderWidth = width;
m_lastRenderHeight = height;
InitRenderer(); InitRenderer();
if (!m_renderer) return; if (!m_renderer) return;
@@ -503,7 +605,9 @@ void UsdSceneRenderer::Render(int width, int height)
m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA); m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA);
if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; } if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; }
m_drawTarget->Bind(); m_drawTarget->Bind();
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA); // RGBA16F: Hydra's internal render buffers carry linear HDR values;
// RGBA16F on the draw target preserves precision in the corrected output.
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F);
m_drawTarget->AddAttachment("depth", m_drawTarget->AddAttachment("depth",
GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F); GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F);
m_drawTarget->Unbind(); m_drawTarget->Unbind();
@@ -560,7 +664,13 @@ void UsdSceneRenderer::Render(int width, int height)
// Camera headlight: point light (w=1) positioned at the camera world-origin, // Camera headlight: point light (w=1) positioned at the camera world-origin,
// transformed by the view-inverse so it tracks the camera each frame. // transformed by the view-inverse so it tracks the camera each frame.
// (stageView.py: l.position = cam_pos + (1,); l.transform = frustum.ComputeViewInverse()) // (stageView.py: l.position = cam_pos + (1,); l.transform = frustum.ComputeViewInverse())
if (m_ambientLightOnly) { //
// The headlight is the *default* fill used only while the stage has no
// authored lights. Once the scene contains real UsdLux lights, suppress it
// so the scene is lit purely by those lights (Hydra evaluates them via
// enableSceneLights) -- mirrors usdview's "use scene lights when present".
const bool stageHasLights = StageHasAuthoredLights();
if (m_ambientLightOnly && !stageHasLights) {
pxr::GfMatrix4d viewInverse = m_viewMatrix.GetInverse(); pxr::GfMatrix4d viewInverse = m_viewMatrix.GetInverse();
pxr::GfVec3d camPos = viewInverse.ExtractTranslation(); pxr::GfVec3d camPos = viewInverse.ExtractTranslation();
@@ -595,15 +705,31 @@ void UsdSceneRenderer::Render(int width, int height)
// --- Render params (matches stageView.renderSinglePass) --- // --- Render params (matches stageView.renderSinglePass) ---
m_renderParams = pxr::UsdImagingGLRenderParams(); m_renderParams = pxr::UsdImagingGLRenderParams();
m_renderParams.frame = m_currentTime; m_renderParams.frame = m_currentTime;
m_renderParams.complexity = 1.0f; m_renderParams.complexity = m_complexity;
// Apply shading mode → drawMode + enableLighting
switch (m_shadingMode) {
case ShadingMode::FlatShaded:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_FLAT;
m_renderParams.enableLighting = true; break;
case ShadingMode::WireframeOnSurface:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME_ON_SURFACE;
m_renderParams.enableLighting = true; break;
case ShadingMode::Wireframe:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME;
m_renderParams.enableLighting = false; break;
case ShadingMode::Unlit:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH; m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
m_renderParams.showGuides = true; m_renderParams.enableLighting = false; break;
m_renderParams.showProxy = true; default: // SmoothShaded
m_renderParams.showRender = false; m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
m_renderParams.enableLighting = true; m_renderParams.enableLighting = true; break;
}
m_renderParams.showGuides = m_showGuides;
m_renderParams.showProxy = m_showProxy;
m_renderParams.showRender = m_showRender;
m_renderParams.enableSampleAlphaToCoverage = true; m_renderParams.enableSampleAlphaToCoverage = true;
m_renderParams.gammaCorrectColors = false; m_renderParams.gammaCorrectColors = false;
m_renderParams.cullStyle = pxr::UsdImagingGLCullStyle::CULL_STYLE_BACK_UNLESS_DOUBLE_SIDED; m_renderParams.cullStyle = static_cast<pxr::UsdImagingGLCullStyle>(m_cullStyle);
m_renderParams.enableSceneMaterials = true; m_renderParams.enableSceneMaterials = true;
m_renderParams.enableSceneLights = true; m_renderParams.enableSceneLights = true;
m_renderParams.highlight = true; m_renderParams.highlight = true;
@@ -612,7 +738,37 @@ void UsdSceneRenderer::Render(int width, int height)
m_renderParams.forceRefresh = m_forceRefresh; m_renderParams.forceRefresh = m_forceRefresh;
m_renderParams.clipPlanes = m_clipPlanes; m_renderParams.clipPlanes = m_clipPlanes;
// Color correction — stageView.py approach: pass the actual mode + OCIO
// params to HdxColorCorrectionTask via RenderParams and SetColorCorrection-
// Settings (mirrors stageView.renderSinglePass).
pxr::TfToken ccToken("disabled");
if (m_colorCorrectionMode == ColorCorrectionMode::sRGB)
ccToken = pxr::TfToken("sRGB");
else if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO)
ccToken = pxr::TfToken("openColorIO");
m_renderParams.colorCorrectionMode = ccToken;
if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO) {
m_renderParams.ocioDisplay = pxr::TfToken(m_ocioDisplay);
m_renderParams.ocioView = pxr::TfToken(m_ocioView);
m_renderParams.ocioColorSpace = pxr::TfToken(m_ocioColorSpace);
m_renderParams.ocioLook = pxr::TfToken(m_ocioLook);
}
m_renderer->SetColorCorrectionSettings(
ccToken,
pxr::TfToken(m_ocioDisplay),
pxr::TfToken(m_ocioView),
pxr::TfToken(m_ocioColorSpace),
pxr::TfToken(m_ocioLook));
// Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below
// would be skipped, permanently unbalancing the GlfDrawTarget bind stack
// and blacking out every later frame.
try {
m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams); m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams);
} catch (const std::exception& e) {
LOG_ERROR("Hydra render failed: " + std::string(e.what()));
}
m_forceRefresh = false; m_forceRefresh = false;
// --- Optional grid overlay --- // --- Optional grid overlay ---
@@ -653,12 +809,45 @@ uint32_t UsdSceneRenderer::GetColorTextureID()
// Resolve MSAA → regular texture before the caller samples it. // Resolve MSAA → regular texture before the caller samples it.
// Called after all overlay draws (axis, bboxes, camera wireframes) so // Called after all overlay draws (axis, bboxes, camera wireframes) so
// every layer of MSAA-rendered content is included in the resolve. // every layer of MSAA-rendered content is included in the resolve.
// No-op when MSAA is not enabled.
m_drawTarget->Resolve(); m_drawTarget->Resolve();
auto att = m_drawTarget->GetAttachment("color"); auto att = m_drawTarget->GetAttachment("color");
return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0; return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0;
} }
bool UsdSceneRenderer::CaptureFrame(std::vector<uint8_t>& outRGBA)
{
if (!m_drawTarget || m_lastRenderWidth <= 0 || m_lastRenderHeight <= 0)
return false;
int w = m_lastRenderWidth;
int h = m_lastRenderHeight;
// Resolve MSAA so we read from the non-multisampled colour attachment.
m_drawTarget->Resolve();
outRGBA.resize(static_cast<size_t>(w) * h * 4);
// Save/restore the read-framebuffer binding so we don't upset ImGui.
GLint prevFbo = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevFbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, outRGBA.data());
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevFbo));
// OpenGL reads bottom-up; flip to top-down.
int stride = w * 4;
std::vector<uint8_t> row(stride);
for (int y = 0; y < h / 2; ++y) {
uint8_t* top = outRGBA.data() + y * stride;
uint8_t* bot = outRGBA.data() + (h - 1 - y) * stride;
std::copy(top, top + stride, row.data());
std::copy(bot, bot + stride, top);
std::copy(row.data(), row.data() + stride, bot);
}
return true;
}
// =========================================================================== // ===========================================================================
// Axis Overlay (stageView.DrawAxis port) // Axis Overlay (stageView.DrawAxis port)
// =========================================================================== // ===========================================================================
@@ -1046,6 +1235,30 @@ void UsdSceneRenderer::DestroyCamWireResources()
if (m_camWireVBO) { glDeleteBuffers(1, &m_camWireVBO); m_camWireVBO = 0; } if (m_camWireVBO) { glDeleteBuffers(1, &m_camWireVBO); m_camWireVBO = 0; }
} }
void UsdSceneRenderer::InitLightWireResources()
{
if (m_lightWireVAO != 0) return;
if (!m_bboxProgram) return; // reuse the already-compiled bbox shader program
glGenVertexArrays(1, &m_lightWireVAO);
glGenBuffers(1, &m_lightWireVBO);
glBindVertexArray(m_lightWireVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
// Sphere/cylinder gizmos use several circles; budget more than the camera VBO.
glBufferData(GL_ARRAY_BUFFER, 1024 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glBindVertexArray(0);
LOG_INFO("Light wireframe GL resources initialized.");
}
void UsdSceneRenderer::DestroyLightWireResources()
{
if (m_lightWireVAO) { glDeleteVertexArrays(1, &m_lightWireVAO); m_lightWireVAO = 0; }
if (m_lightWireVBO) { glDeleteBuffers(1, &m_lightWireVBO); m_lightWireVBO = 0; }
}
void UsdSceneRenderer::DrawBox( void UsdSceneRenderer::DrawBox(
const pxr::GfRange3d& range, const pxr::GfRange3d& range,
const pxr::GfMatrix4f& mvp) const pxr::GfMatrix4f& mvp)
@@ -1291,6 +1504,26 @@ void UsdSceneRenderer::BuildCameraWireframeLines(const pxr::GfCamera& gfCam,
push(arrowBase, arrowTip); push(arrowBase, arrowTip);
} }
// Builds near quad + far quad + 4 connecting edges for the full GfFrustum.
// Mirrors stageView.py DrawCameraGuides: corners via GfFrustum::ComputeCorners(),
// line pairs via the same 24-index layout. Appends 24 XYZ float triples.
static void BuildCameraGuideLines(const pxr::GfCamera& gfCam,
std::vector<float>& outVerts)
{
const std::vector<pxr::GfVec3d> c = gfCam.GetFrustum().ComputeCorners();
// 0=LBN 1=RBN 2=LTN 3=RTN 4=LBF 5=RBF 6=LTF 7=RTF
static const int kIdx[24] = {
0,1, 1,3, 3,2, 2,0, // near quad
4,5, 5,7, 7,6, 6,4, // far quad
3,7, 0,4, 1,5, 2,6 // connecting edges
};
for (int i : kIdx) {
outVerts.push_back(static_cast<float>(c[i][0]));
outVerts.push_back(static_cast<float>(c[i][1]));
outVerts.push_back(static_cast<float>(c[i][2]));
}
}
// =========================================================================== // ===========================================================================
// DrawCameraWireframes // DrawCameraWireframes
// =========================================================================== // ===========================================================================
@@ -1345,6 +1578,8 @@ void UsdSceneRenderer::DrawCameraWireframes(
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix); pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
std::vector<float> guideVerts; // accumulates frustum oracle lines
for (const auto& camPath : m_cachedCameraPaths) { for (const auto& camPath : m_cachedCameraPaths) {
pxr::UsdPrim prim = stage->GetPrimAtPath(camPath); pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
if (!prim) continue; if (!prim) continue;
@@ -1380,6 +1615,23 @@ void UsdSceneRenderer::DrawCameraWireframes(
glBindVertexArray(m_camWireVAO); glBindVertexArray(m_camWireVAO);
glDrawArrays(GL_LINES, 0, vertCount); glDrawArrays(GL_LINES, 0, vertCount);
glBindVertexArray(0); glBindVertexArray(0);
// Accumulate full-frustum oracle lines for non-active cameras (stageView.DrawCameraGuides)
if (m_showCameraGuide && !isActive)
BuildCameraGuideLines(gfCam, guideVerts);
}
// Draw all camera guide frustum boxes in one call (oracle orange from stageView.py)
if (m_showCameraGuide && !guideVerts.empty()) {
glUniform4f(m_bboxUniformColor, 0.82745f, 0.39608f, 0.16471f, 1.0f);
glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(guideVerts.size() * sizeof(float)),
guideVerts.data(), GL_DYNAMIC_DRAW);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
glBindVertexArray(m_camWireVAO);
glDrawArrays(GL_LINES, 0, static_cast<int>(guideVerts.size() / 3));
glBindVertexArray(0);
} }
// Restore GL state // Restore GL state
@@ -1392,6 +1644,205 @@ void UsdSceneRenderer::DrawCameraWireframes(
m_drawTarget->Unbind(); m_drawTarget->Unbind();
} }
// ===========================================================================
// BuildLightWireframeLines
//
// Geometry is built in the light's LOCAL space (using its schema attributes in
// local units) and every vertex is pushed through localToWorld, so position,
// orientation and scale are all handled uniformly. USD lights emit along local
// -Z (rect/disk/distant); cylinder length runs along local X.
// ===========================================================================
void UsdSceneRenderer::BuildLightWireframeLines(const pxr::UsdPrim& lightPrim,
const pxr::GfMatrix4d& l2w,
double scale,
std::vector<float>& outVerts)
{
using pxr::GfVec3d;
static constexpr int kSeg = 24;
static constexpr double kTwoPi = 6.283185307179586;
auto pushLine = [&](const GfVec3d& a, const GfVec3d& b) {
GfVec3d wa = l2w.Transform(a);
GfVec3d wb = l2w.Transform(b);
outVerts.push_back((float)wa[0]); outVerts.push_back((float)wa[1]); outVerts.push_back((float)wa[2]);
outVerts.push_back((float)wb[0]); outVerts.push_back((float)wb[1]); outVerts.push_back((float)wb[2]);
};
// Circle centred at c, spanned by unit axes u,v, radius r (local space).
auto circle = [&](const GfVec3d& c, const GfVec3d& u, const GfVec3d& v, double r) {
GfVec3d prev;
for (int i = 0; i <= kSeg; ++i) {
double a = kTwoPi * double(i) / double(kSeg);
GfVec3d p = c + u * (r * std::cos(a)) + v * (r * std::sin(a));
if (i > 0) pushLine(prev, p);
prev = p;
}
};
const GfVec3d X(1,0,0), Y(0,1,0), Z(0,0,1), O(0,0,0);
const double marker = scale * 0.5; // display-scaled size for direction lines / markers
if (lightPrim.IsA<pxr::UsdLuxRectLight>()) {
float w = 1.0f, h = 1.0f;
pxr::UsdLuxRectLight rl(lightPrim);
rl.GetWidthAttr().Get(&w, m_currentTime);
rl.GetHeightAttr().Get(&h, m_currentTime);
double hw = w * 0.5, hh = h * 0.5;
GfVec3d bl(-hw,-hh,0), br(hw,-hh,0), tr(hw,hh,0), tl(-hw,hh,0);
pushLine(bl,br); pushLine(br,tr); pushLine(tr,tl); pushLine(tl,bl);
pushLine(O, GfVec3d(0,0,-marker)); // emission direction (-Z)
}
else if (lightPrim.IsA<pxr::UsdLuxDiskLight>()) {
float r = 0.5f; pxr::UsdLuxDiskLight(lightPrim).GetRadiusAttr().Get(&r, m_currentTime);
circle(O, X, Y, r);
pushLine(O, GfVec3d(0,0,-marker));
}
else if (lightPrim.IsA<pxr::UsdLuxCylinderLight>()) {
float r = 0.5f, len = 1.0f;
pxr::UsdLuxCylinderLight cl(lightPrim);
cl.GetRadiusAttr().Get(&r, m_currentTime);
cl.GetLengthAttr().Get(&len, m_currentTime);
double hx = len * 0.5;
circle(GfVec3d( hx,0,0), Y, Z, r); // end caps (in local YZ)
circle(GfVec3d(-hx,0,0), Y, Z, r);
pushLine(GfVec3d(-hx, r,0), GfVec3d(hx, r,0)); // connecting lines
pushLine(GfVec3d(-hx,-r,0), GfVec3d(hx,-r,0));
pushLine(GfVec3d(-hx,0, r), GfVec3d(hx,0, r));
pushLine(GfVec3d(-hx,0,-r), GfVec3d(hx,0,-r));
}
else if (lightPrim.IsA<pxr::UsdLuxDistantLight>()) {
// Sun: small disc facing -Z plus parallel rays along -Z.
double r = marker;
circle(O, X, Y, r);
const GfVec3d off[5] = { O, GfVec3d(r,0,0), GfVec3d(-r,0,0), GfVec3d(0,r,0), GfVec3d(0,-r,0) };
for (const auto& o : off) pushLine(o, o + GfVec3d(0,0,-marker*2.0));
}
else if (lightPrim.IsA<pxr::UsdLuxDomeLight>()) {
// Environment dome: 3 large display-scaled circles around the origin.
double r = std::max(scale, 1.0);
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
}
else if (lightPrim.IsA<pxr::UsdLuxSphereLight>()) {
float r = 0.5f; pxr::UsdLuxSphereLight sl(lightPrim);
sl.GetRadiusAttr().Get(&r, m_currentTime);
bool pointish = false; sl.GetTreatAsPointAttr().Get(&pointish, m_currentTime);
if (pointish || r < 1e-4f) {
double s = marker; // point light: axis cross + tiny disc
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
circle(O, X, Y, s * 0.4);
} else {
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
}
}
else {
double s = marker; // unknown light: generic point marker
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
}
}
// ===========================================================================
// StageHasAuthoredLights
// ===========================================================================
bool UsdSceneRenderer::StageHasAuthoredLights() const
{
if (!m_stage) return false;
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
return true;
}
return false;
}
// ===========================================================================
// DrawLightWireframes (mirrors DrawCameraWireframes)
// ===========================================================================
void UsdSceneRenderer::DrawLightWireframes(
pxr::UsdStageRefPtr stage,
const pxr::SdfPathVector& selectedPaths,
const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist)
{
if (!stage) return;
if (!m_bboxProgram || !m_lightWireVAO || !m_lightWireVBO) return;
if (!m_drawTarget) return;
// Collect every light prim (anything carrying UsdLuxLightAPI).
pxr::SdfPathVector lightPaths;
for (const pxr::UsdPrim& prim : stage->Traverse()) {
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
lightPaths.push_back(prim.GetPath());
}
if (lightPaths.empty()) return;
double rawScale = viewportCameraDist * 0.12;
double scale = std::min(rawScale, 50.0);
scale = std::max(scale, 0.5);
std::unordered_set<std::string> selectedSet;
for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevDepthMask; glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
GLboolean prevBlend = glIsEnabled(GL_BLEND);
// Draw on top of everything (like camera wireframes) so lights stay visible.
glDisable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);
glUseProgram(m_bboxProgram);
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
pxr::UsdGeomXformCache xformCache(m_currentTime);
for (const auto& lpath : lightPaths) {
pxr::UsdPrim prim = stage->GetPrimAtPath(lpath);
if (!prim) continue;
pxr::GfMatrix4d l2w = xformCache.GetLocalToWorldTransform(prim);
bool isSelected = (selectedSet.count(lpath.GetString()) > 0);
pxr::GfVec4f col = isSelected
? pxr::GfVec4f(1.00f, 0.75f, 0.10f, 1.00f) // accent orange
: pxr::GfVec4f(1.00f, 0.90f, 0.35f, 0.90f); // warm yellow
glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);
std::vector<float> verts;
BuildLightWireframeLines(prim, l2w, scale, verts);
if (verts.empty()) continue;
int vertCount = static_cast<int>(verts.size() / 3);
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
verts.data(), GL_DYNAMIC_DRAW);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
glBindVertexArray(m_lightWireVAO);
glDrawArrays(GL_LINES, 0, vertCount);
glBindVertexArray(0);
}
if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
if (prevDepthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
glDepthMask(prevDepthMask);
if (!prevBlend) glDisable(GL_BLEND);
glUseProgram(0);
m_drawTarget->Unbind();
}
// =========================================================================== // ===========================================================================
// PickCameraAtPoint // PickCameraAtPoint
// =========================================================================== // ===========================================================================
+131 -2
View File
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include <glad/gl.h> #include <glad/gl.h>
@@ -6,6 +6,8 @@
#include <pxr/usd/usd/timeCode.h> #include <pxr/usd/usd/timeCode.h>
#include <pxr/imaging/glf/drawTarget.h> #include <pxr/imaging/glf/drawTarget.h>
#include <pxr/usdImaging/usdImagingGL/engine.h> #include <pxr/usdImaging/usdImagingGL/engine.h>
#include <pxr/usdImaging/usdImagingGL/rendererSettings.h>
#include <pxr/base/vt/value.h>
#include <pxr/imaging/glf/simpleLight.h> #include <pxr/imaging/glf/simpleLight.h>
#include <pxr/imaging/glf/simpleMaterial.h> #include <pxr/imaging/glf/simpleMaterial.h>
#include <pxr/base/gf/vec3f.h> #include <pxr/base/gf/vec3f.h>
@@ -21,6 +23,7 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <memory>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -31,6 +34,31 @@ enum class BBoxMode {
AllSelection ///< One combined box for the entire selection AllSelection ///< One combined box for the entire selection
}; };
/// Surface shading mode — maps to UsdImagingGLDrawMode + enableLighting.
enum class ShadingMode {
SmoothShaded, ///< Smooth shading with materials (default)
FlatShaded, ///< Flat shading with materials
WireframeOnSurface, ///< Wireframe overlay on solid surface
Wireframe, ///< Wireframe only, no fill
Unlit, ///< Smooth geometry, lighting disabled
};
/// Back-face culling style — maps directly to UsdImagingGLCullStyle.
enum class CullStyle {
NoOpinion = 0,
Nothing = 1,
Back = 2,
Front = 3,
BackUnlessDoubleSided = 4,
};
/// Viewport color correction mode — maps to UsdImagingGLRenderParams::colorCorrectionMode.
enum class ColorCorrectionMode {
Disabled, ///< No correction (raw linear output)
sRGB, ///< Linear → sRGB gamma (default)
OpenColorIO, ///< Full OCIO pipeline via bundled ACES 1.2 config
};
class UsdSceneRenderer { class UsdSceneRenderer {
public: public:
UsdSceneRenderer(); UsdSceneRenderer();
@@ -108,6 +136,16 @@ public:
const pxr::GfMatrix4d& viewProjMatrix, const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist); double viewportCameraDist);
/// Draw wireframe gizmos for all UsdLux light prims in the stage.
/// The shape reflects each light's type (sphere/rect/disk/distant/dome/
/// cylinder); selected lights get an accent colour. viewportCameraDist
/// scales direction lines and point/distant/dome markers.
void DrawLightWireframes(
pxr::UsdStageRefPtr stage,
const pxr::SdfPathVector& selectedPaths,
const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist);
/// Test a screen-space mouse position against all camera wireframe segments. /// Test a screen-space mouse position against all camera wireframe segments.
/// Returns true and sets *outCameraPath if a camera wireframe is within /// Returns true and sets *outCameraPath if a camera wireframe is within
/// kCameraPickRadius (10 px) of mousePos. Prioritises the closest hit. /// kCameraPickRadius (10 px) of mousePos. Prioritises the closest hit.
@@ -147,12 +185,42 @@ public:
/// unknown or the switch fails. The renderer is re-initialised if needed. /// unknown or the switch fails. The renderer is re-initialised if needed.
bool SetRendererPlugin(const pxr::TfToken& pluginId); bool SetRendererPlugin(const pxr::TfToken& pluginId);
/// False while a progressive delegate (Arnold/Cycles/Embree…) is still
/// accumulating samples for the last-submitted frame; Storm is always
/// converged after one pass. Callers that cache the output texture must
/// keep calling Render() until this is true.
bool IsConverged() const { return !m_renderer || m_renderer->IsConverged(); }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// View settings // View settings
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
bool ShowGrid() const { return m_showGrid; } bool ShowGrid() const { return m_showGrid; }
void SetShowGrid(bool show) { m_showGrid = show; } void SetShowGrid(bool show) { m_showGrid = show; }
/// Hydra refinement complexity (1.0 = base mesh, up to 2.0). The shader
/// ball uses a raised value so the sphere tessellates smoothly.
float Complexity() const { return m_complexity; }
void SetComplexity(float c) { m_complexity = c; }
bool ShowCameraGuide() const { return m_showCameraGuide; }
void SetShowCameraGuide(bool show) { m_showCameraGuide = show; }
bool ShowGuides() const { return m_showGuides; }
void SetShowGuides(bool show) { m_showGuides = show; }
bool ShowProxy() const { return m_showProxy; }
void SetShowProxy(bool show) { m_showProxy = show; }
bool ShowRender() const { return m_showRender; }
void SetShowRender(bool show) { m_showRender = show; }
const pxr::TfToken& GetCurrentAov() const { return m_currentAov; }
pxr::TfTokenVector GetRendererAovs() const;
bool SetCurrentAov(const pxr::TfToken& aov);
pxr::UsdImagingGLRendererSettingsList GetRendererSettingsList() const;
pxr::VtValue GetRendererSetting(const pxr::TfToken& id) const;
void SetRendererSetting(const pxr::TfToken& id,
const pxr::VtValue& value);
bool GetAAEnabled() const { return m_aaEnabled; } bool GetAAEnabled() const { return m_aaEnabled; }
void SetAAEnabled(bool enabled) { m_aaEnabled = enabled; } void SetAAEnabled(bool enabled) { m_aaEnabled = enabled; }
@@ -189,6 +257,24 @@ public:
bool GetDomeLightEnabled() const { return m_domeLightEnabled; } bool GetDomeLightEnabled() const { return m_domeLightEnabled; }
void SetDomeLightEnabled(bool val) { m_domeLightEnabled = val; } void SetDomeLightEnabled(bool val) { m_domeLightEnabled = val; }
ShadingMode GetShadingMode() const { return m_shadingMode; }
void SetShadingMode(ShadingMode m) { m_shadingMode = m; m_forceRefresh = true; }
CullStyle GetCullStyle() const { return m_cullStyle; }
void SetCullStyle(CullStyle c) { m_cullStyle = c; m_forceRefresh = true; }
ColorCorrectionMode GetColorCorrectionMode() const { return m_colorCorrectionMode; }
void SetColorCorrectionMode(ColorCorrectionMode m) { m_colorCorrectionMode = m; m_forceRefresh = true; }
const std::string& GetOcioDisplay() const { return m_ocioDisplay; }
void SetOcioDisplay(std::string v) { m_ocioDisplay = std::move(v); m_forceRefresh = true; }
const std::string& GetOcioView() const { return m_ocioView; }
void SetOcioView(std::string v) { m_ocioView = std::move(v); m_forceRefresh = true; }
const std::string& GetOcioColorSpace() const { return m_ocioColorSpace; }
void SetOcioColorSpace(std::string v){ m_ocioColorSpace = std::move(v); m_forceRefresh = true; }
const std::string& GetOcioLook() const { return m_ocioLook; }
void SetOcioLook(std::string v) { m_ocioLook = std::move(v); m_forceRefresh = true; }
/// Default material ambient (kA, default 0.2 — matches viewSettingsDataModel.py). /// Default material ambient (kA, default 0.2 — matches viewSettingsDataModel.py).
float GetDefaultMaterialAmbient() const { return m_defaultMaterialAmbient; } float GetDefaultMaterialAmbient() const { return m_defaultMaterialAmbient; }
void SetDefaultMaterialAmbient(float v) { m_defaultMaterialAmbient = v; } void SetDefaultMaterialAmbient(float v) { m_defaultMaterialAmbient = v; }
@@ -202,11 +288,22 @@ public:
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
uint32_t GetColorTextureID(); uint32_t GetColorTextureID();
/// Dimensions of the most recent Render() call.
int GetLastRenderWidth() const { return m_lastRenderWidth; }
int GetLastRenderHeight() const { return m_lastRenderHeight; }
/// Read the last rendered frame into top-down RGBA8 pixels.
/// Resolves MSAA before reading. Returns false if the draw target is not ready.
bool CaptureFrame(std::vector<uint8_t>& outRGBA);
// For unit tests // For unit tests
pxr::GlfDrawTargetRefPtr GetDrawTargetForTest() const { return m_drawTarget; } pxr::GlfDrawTargetRefPtr GetDrawTargetForTest() const { return m_drawTarget; }
private: private:
int m_lastRenderWidth = 0;
int m_lastRenderHeight = 0;
void InitRenderer(); void InitRenderer();
void InitGridResources(); void InitGridResources();
void RebuildGridVBO(); ///< (Re)build grid line geometry after up-axis or size change. void RebuildGridVBO(); ///< (Re)build grid line geometry after up-axis or size change.
void DestroyGridResources(); void DestroyGridResources();
@@ -217,6 +314,8 @@ private:
void DestroyBBoxResources(); void DestroyBBoxResources();
void InitCamWireResources(); void InitCamWireResources();
void DestroyCamWireResources(); void DestroyCamWireResources();
void InitLightWireResources();
void DestroyLightWireResources();
/// Draw a single axis-aligned box from a GfRange3d. /// Draw a single axis-aligned box from a GfRange3d.
void DrawBox(const pxr::GfRange3d& range, const pxr::GfMatrix4f& mvp); void DrawBox(const pxr::GfRange3d& range, const pxr::GfMatrix4f& mvp);
@@ -227,6 +326,18 @@ private:
double scale, double scale,
std::vector<float>& outVerts); std::vector<float>& outVerts);
/// Build light-gizmo line segments (GL_LINES vertex pairs) in world space for
/// a single light prim. localToWorld is the light's transform at m_currentTime;
/// scale is the display-size factor (for direction lines / point markers).
void BuildLightWireframeLines(const pxr::UsdPrim& lightPrim,
const pxr::GfMatrix4d& localToWorld,
double scale,
std::vector<float>& outVerts);
/// True if the stage contains at least one authored UsdLux light.
/// Used to suppress the default camera headlight when real lights exist.
bool StageHasAuthoredLights() const;
/// Project a world-space point to absolute screen coordinates (x=imagePosX+pixelX, etc.). /// Project a world-space point to absolute screen coordinates (x=imagePosX+pixelX, etc.).
/// Returns false when the point is behind the camera. /// Returns false when the point is behind the camera.
static bool WorldToScreen(const pxr::GfVec3d& world, static bool WorldToScreen(const pxr::GfVec3d& world,
@@ -242,7 +353,7 @@ private:
pxr::UsdStageRefPtr m_stage; pxr::UsdStageRefPtr m_stage;
std::shared_ptr<pxr::UsdImagingGLEngine> m_renderer; std::shared_ptr<pxr::UsdImagingGLEngine> m_renderer;
pxr::TfToken m_currentRendererPlugin; ///< Active plugin ID (empty = default) pxr::TfToken m_currentRendererPlugin; ///< empty = use Storm/GL heuristic on init
pxr::GlfDrawTargetRefPtr m_drawTarget; pxr::GlfDrawTargetRefPtr m_drawTarget;
pxr::UsdImagingGLRenderParams m_renderParams; pxr::UsdImagingGLRenderParams m_renderParams;
@@ -260,10 +371,23 @@ private:
std::vector<pxr::GfVec4d> m_clipPlanes; std::vector<pxr::GfVec4d> m_clipPlanes;
bool m_showGrid; bool m_showGrid;
float m_complexity = 1.0f;
bool m_showCameraGuide;
bool m_showGuides;
bool m_showProxy;
bool m_showRender;
pxr::TfToken m_currentAov;
bool m_aaEnabled; ///< GL_LINE_SMOOTH anti-aliasing for all line overlays bool m_aaEnabled; ///< GL_LINE_SMOOTH anti-aliasing for all line overlays
pxr::GfVec3f m_backgroundColor; pxr::GfVec3f m_backgroundColor;
// Lighting settings (mirrors stageView.py viewSettings) // Lighting settings (mirrors stageView.py viewSettings)
ShadingMode m_shadingMode; // draw mode + lighting flags
CullStyle m_cullStyle; // back/front/none face culling
ColorCorrectionMode m_colorCorrectionMode; // viewport color correction
std::string m_ocioDisplay;
std::string m_ocioView;
std::string m_ocioColorSpace;
std::string m_ocioLook;
bool m_ambientLightOnly; // camera headlight bool m_ambientLightOnly; // camera headlight
bool m_domeLightEnabled; // dome/IBL light bool m_domeLightEnabled; // dome/IBL light
bool m_stageIsZup; // used for dome light rotation bool m_stageIsZup; // used for dome light rotation
@@ -307,9 +431,14 @@ private:
GLuint m_camWireVAO = 0; GLuint m_camWireVAO = 0;
GLuint m_camWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed GLuint m_camWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed
// --- Light wireframe GL resources (mirrors camera wireframe) ---
GLuint m_lightWireVAO = 0;
GLuint m_lightWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed
// --- Camera wireframe cache --- // --- Camera wireframe cache ---
pxr::SdfPathVector m_cachedCameraPaths; pxr::SdfPathVector m_cachedCameraPaths;
bool m_cameraCacheDirty = true; bool m_cameraCacheDirty = true;
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
@@ -0,0 +1,101 @@
#include "ConnectShaderAttrsCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
namespace UsdLayerManager {
ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
, m_destType(destType)
, m_sourceNode(sourceNode)
, m_sourceOutput(sourceOutput)
, m_sourceType(sourceType)
, m_editLayer(editLayer)
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
" -> " + destNode.GetName() + "." + destInput)
{
if (!stage) return;
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
if (!destShader) return;
pxr::UsdShadeInput existingInput = destShader.GetInput(pxr::TfToken(destInput));
if (!existingInput) return;
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(existingInput)) {
if (!source.IsValid()) continue;
m_hadPriorConnection = true;
m_priorSourceNode = source.source.GetPrim().GetPath();
m_priorSourceOutput = source.sourceName.GetString();
break; // shader inputs carry a single upstream connection in this editor's model
}
}
void ConnectShaderAttrsCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
if (!destShader || !sourceShader) {
LOG_ERROR("ConnectShaderAttrsCommand: invalid shader prim(s)");
return;
}
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
} else {
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
}
}
void ConnectShaderAttrsCommand::Undo() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
if (!destShader) return;
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
if (!destInput) return;
auto doUndo = [&]() {
if (m_hadPriorConnection) {
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
if (priorSourceShader && priorSourceOutput)
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
}
};
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
doUndo();
} else {
doUndo();
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,52 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
namespace UsdLayerManager {
/// Connects a shader input to an upstream shader output, sparsely authoring
/// both attributes (with the given types) if they don't already exist.
/// Captures whatever the input was previously connected to (if anything) so
/// Undo can restore it exactly rather than merely disconnecting.
class ConnectShaderAttrsCommand : public ICommand {
public:
/// editLayer: layer to author into (mirrors TransformCommand's pattern).
/// Captured at construction time so Undo() targets the same layer as
/// Execute() even if the stage's ambient edit target changes in between
/// (e.g. a render-layer switch) — pass null/empty to fall back to
/// whatever the ambient edit target is at call time (today's behavior).
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode;
std::string m_destInput;
pxr::SdfValueTypeName m_destType;
pxr::SdfPath m_sourceNode;
std::string m_sourceOutput;
pxr::SdfValueTypeName m_sourceType;
pxr::SdfLayerHandle m_editLayer;
std::string m_description;
bool m_hadPriorConnection = false;
pxr::SdfPath m_priorSourceNode;
std::string m_priorSourceOutput;
};
} // namespace UsdLayerManager
@@ -0,0 +1,46 @@
#include "CreateShaderNodeCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h>
namespace UsdLayerManager {
CreateShaderNodeCommand::CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& path,
const std::string& shaderId,
const pxr::GfVec2f& uiPosition)
: m_stage(stage)
, m_path(path)
, m_shaderId(shaderId)
, m_uiPosition(uiPosition)
, m_description("Create Shader " + shaderId + " " + path.GetString())
{
}
void CreateShaderNodeCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(m_stage, m_path);
if (!shader) {
LOG_ERROR("CreateShaderNodeCommand: failed to define shader " + m_path.GetString());
return;
}
shader.SetShaderId(pxr::TfToken(m_shaderId));
shader.GetPrim().SetCustomDataByKey(pxr::TfToken("uiPosition"), pxr::VtValue(m_uiPosition));
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreateShaderNodeCommand::Execute error: ") + e.what());
}
}
void CreateShaderNodeCommand::Undo() {
if (!m_stage) return;
try {
if (!m_stage->RemovePrim(m_path))
LOG_ERROR("CreateShaderNodeCommand: failed to remove prim " + m_path.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreateShaderNodeCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,34 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/gf/vec2f.h>
#include <string>
namespace UsdLayerManager {
/// Creates a UsdShadeShader prim with the given Sdr shader identifier and
/// authors its canvas position as uiPosition custom data (see
/// docs/adr — Material Editor node-graph layout is stored as prim
/// customData, not scene data, so it never affects composition/renders).
class CreateShaderNodeCommand : public ICommand {
public:
CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& path,
const std::string& shaderId,
const pxr::GfVec2f& uiPosition);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_path;
std::string m_shaderId;
pxr::GfVec2f m_uiPosition;
std::string m_description;
};
} // namespace UsdLayerManager
+8
View File
@@ -59,6 +59,14 @@ void DeletePrimCommand::Undo() {
if (!rootLayer) return; if (!rootLayer) return;
try { try {
// The destination ancestors may be gone by now (e.g. the parent
// material was itself deleted after this command ran) — SdfCopySpec
// fails with "No spec ... when trying to set field 'primChildren'"
// when the parent spec is missing, so recreate the chain first.
pxr::SdfPath parentPath = m_primPath.GetParentPath();
if (!parentPath.IsAbsoluteRootPath() && !rootLayer->GetPrimAtPath(parentPath))
pxr::SdfCreatePrimInLayer(rootLayer, parentPath);
if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath)) if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath))
LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString()); LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString());
} catch (const std::exception& e) { } catch (const std::exception& e) {
@@ -0,0 +1,78 @@
#include "DisconnectShaderAttrCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
namespace UsdLayerManager {
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
pxr::SdfLayerHandle editLayer)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
, m_editLayer(editLayer)
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
{
if (!stage) return;
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(destInput));
if (!input) return;
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(input)) {
if (!source.IsValid()) continue;
m_hadConnection = true;
m_sourceNode = source.source.GetPrim().GetPath();
m_sourceOutput = source.sourceName.GetString();
break;
}
}
void DisconnectShaderAttrCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
if (!input) return;
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
}
}
void DisconnectShaderAttrCommand::Undo() {
if (!m_stage || !m_hadConnection) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
if (!destShader || !sourceShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
if (!input || !output) return;
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
} else {
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,39 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
namespace UsdLayerManager {
/// Disconnects a shader input from its upstream source, capturing that
/// source at construction time so Undo can restore it.
class DisconnectShaderAttrCommand : public ICommand {
public:
/// editLayer: see ConnectShaderAttrsCommand's constructor doc — same
/// capture-at-construction-time pattern, null/empty falls back to the
/// ambient edit target at call time (today's behavior).
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode;
std::string m_destInput;
pxr::SdfLayerHandle m_editLayer;
std::string m_description;
bool m_hadConnection = false;
pxr::SdfPath m_sourceNode;
std::string m_sourceOutput;
};
} // namespace UsdLayerManager
+113
View File
@@ -0,0 +1,113 @@
#include "GroupPrimsCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/namespaceEditor.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/base/tf/token.h>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
GroupPrimsCommand::GroupPrimsCommand(UsdStageRefPtr stage,
const std::vector<SdfPath>& srcPaths,
const SdfPath& groupPath)
: m_stage(stage)
, m_groupPath(groupPath)
, m_srcPaths(srcPaths)
, m_description("Group " + std::to_string(srcPaths.size()) +
" prim(s) under " + groupPath.GetString())
{
}
/// Compute a unique child name under @p parent (collision check against live stage).
static std::string UniqueNameUnder(UsdStageRefPtr stage,
const SdfPath& parent,
const std::string& baseName) {
std::string name = baseName;
int n = 1;
while (stage->GetPrimAtPath(parent.AppendChild(TfToken(name))).IsValid())
name = baseName + "_" + std::to_string(n++);
return name;
}
static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) {
if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath())
return stage->GetPseudoRoot();
return stage->GetPrimAtPath(parentPath);
}
void GroupPrimsCommand::Execute() {
// Create the Xform group.
UsdPrim groupPrim = m_stage->DefinePrim(m_groupPath, TfToken("Xform"));
if (!groupPrim.IsValid()) {
LOG_ERROR("GroupPrimsCommand::Execute: failed to create group: " + m_groupPath.GetString());
return;
}
m_movedNames.clear();
for (const SdfPath& srcPath : m_srcPaths) {
UsdPrim src = m_stage->GetPrimAtPath(srcPath);
if (!src.IsValid()) {
LOG_WARNING("GroupPrimsCommand::Execute: prim not found (skipping): " + srcPath.GetString());
m_movedNames.emplace_back(srcPath.GetName());
continue;
}
// Compute a collision-free name inside the group.
std::string name = UniqueNameUnder(m_stage, m_groupPath, srcPath.GetName());
m_movedNames.push_back(name);
UsdPrim gPrim = m_stage->GetPrimAtPath(m_groupPath);
UsdNamespaceEditor editor(m_stage);
editor.ReparentPrim(src, gPrim, TfToken(name));
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("GroupPrimsCommand::Execute: cannot reparent " + srcPath.GetString() + ": " + whyNot);
continue;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("GroupPrimsCommand::Execute: ApplyEdits failed for " + srcPath.GetString());
}
}
}
void GroupPrimsCommand::Undo() {
if (m_movedNames.size() != m_srcPaths.size()) {
LOG_ERROR("GroupPrimsCommand::Undo: state mismatch, cannot undo safely");
return;
}
// Reparent children back in reverse order.
for (int i = (int)m_srcPaths.size() - 1; i >= 0; --i) {
SdfPath movedPath = m_groupPath.AppendChild(TfToken(m_movedNames[i]));
UsdPrim moved = m_stage->GetPrimAtPath(movedPath);
if (!moved.IsValid()) {
LOG_WARNING("GroupPrimsCommand::Undo: prim not in group (skipping): " + movedPath.GetString());
continue;
}
SdfPath origParentPath = m_srcPaths[i].GetParentPath();
UsdPrim origParent = GetParentPrim(m_stage, origParentPath);
if (!origParent.IsValid()) {
LOG_ERROR("GroupPrimsCommand::Undo: original parent not found: " + origParentPath.GetString());
continue;
}
UsdNamespaceEditor editor(m_stage);
editor.ReparentPrim(moved, origParent, TfToken(m_srcPaths[i].GetName()));
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("GroupPrimsCommand::Undo: cannot reparent back " + movedPath.GetString() + ": " + whyNot);
continue;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("GroupPrimsCommand::Undo: ApplyEdits failed for " + movedPath.GetString());
}
}
// Remove the (now empty) group prim.
m_stage->RemovePrim(m_groupPath);
}
} // namespace UsdLayerManager
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
/// Creates a new Xform group prim and reparents the given prims under it.
/// The group path must be pre-computed (collision-free) by the caller.
/// Undo reparents each child back to its original parent and removes the group.
class GroupPrimsCommand : public ICommand {
public:
GroupPrimsCommand(pxr::UsdStageRefPtr stage,
const std::vector<pxr::SdfPath>& srcPaths,
const pxr::SdfPath& groupPath);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_groupPath;
std::vector<pxr::SdfPath> m_srcPaths; ///< original prim paths (before group)
std::vector<std::string> m_movedNames; ///< actual names used inside group (set in Execute)
std::string m_description;
};
} // namespace UsdLayerManager
+65
View File
@@ -0,0 +1,65 @@
#include "RenamePrimCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/namespaceEditor.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/base/tf/token.h>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
RenamePrimCommand::RenamePrimCommand(UsdStageRefPtr stage,
const SdfPath& primPath,
const std::string& newName)
: m_stage(stage)
, m_oldPath(primPath)
, m_oldName(primPath.GetName())
, m_newName(newName)
, m_description("Rename " + primPath.GetName() + " \xe2\x86\x92 " + newName)
{
m_newPath = primPath.GetParentPath().AppendChild(TfToken(newName));
}
void RenamePrimCommand::Execute() {
UsdPrim prim = m_stage->GetPrimAtPath(m_oldPath);
if (!prim.IsValid()) {
LOG_ERROR("RenamePrimCommand::Execute: prim not found: " + m_oldPath.GetString());
return;
}
UsdNamespaceEditor editor(m_stage);
if (!editor.RenamePrim(prim, TfToken(m_newName))) {
LOG_ERROR("RenamePrimCommand::Execute: RenamePrim() rejected for " + m_oldPath.GetString());
return;
}
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot);
return;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("RenamePrimCommand::Execute: ApplyEdits failed for " + m_oldPath.GetString());
}
}
void RenamePrimCommand::Undo() {
UsdPrim prim = m_stage->GetPrimAtPath(m_newPath);
if (!prim.IsValid()) {
LOG_ERROR("RenamePrimCommand::Undo: prim not found: " + m_newPath.GetString());
return;
}
UsdNamespaceEditor editor(m_stage);
if (!editor.RenamePrim(prim, TfToken(m_oldName))) {
LOG_ERROR("RenamePrimCommand::Undo: RenamePrim() rejected for " + m_newPath.GetString());
return;
}
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot);
return;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("RenamePrimCommand::Undo: ApplyEdits failed for " + m_newPath.GetString());
}
}
} // namespace UsdLayerManager
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <string>
namespace UsdLayerManager {
/// Renames a USD prim via UsdNamespaceEditor, updating all composition arcs.
/// Undo restores the original name.
class RenamePrimCommand : public ICommand {
public:
RenamePrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
const std::string& newName);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_oldPath; ///< path before rename
pxr::SdfPath m_newPath; ///< path after rename (set in Execute)
std::string m_oldName;
std::string m_newName;
std::string m_description;
};
} // namespace UsdLayerManager
+102
View File
@@ -0,0 +1,102 @@
#include "RenderLayerCommands.h"
namespace UsdLayerManager {
// ─── RenderLayerCreateCommand ─────────────────────────────────────────────
RenderLayerCreateCommand::RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name)
: m_mgr(mgr)
, m_name(std::move(name))
, m_description("Create Render Layer " + m_name)
{
}
void RenderLayerCreateCommand::Execute() {
if (!m_mgr) return;
m_layerId = m_mgr->CreateRenderLayer(m_name);
}
void RenderLayerCreateCommand::Undo() {
if (!m_mgr || m_layerId.empty()) return;
m_mgr->DeleteRenderLayer(m_layerId);
}
// ─── RenderLayerDeleteCommand ─────────────────────────────────────────────
RenderLayerDeleteCommand::RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_description("Delete Render Layer")
{
if (!mgr) return;
for (const auto& info : mgr->GetRenderLayers()) {
if (!info.isDefault && info.layerIdentifier == m_layerId) {
m_description = "Delete Render Layer " + info.name;
break;
}
}
}
void RenderLayerDeleteCommand::Execute() {
if (!m_mgr) return;
m_mgr->DeleteRenderLayer(m_layerId);
}
void RenderLayerDeleteCommand::Undo() {
if (!m_mgr || m_layerId.empty()) return;
m_mgr->RestoreDeletedRenderLayer(m_layerId);
}
// ─── RenderLayerRenameCommand ─────────────────────────────────────────────
RenderLayerRenameCommand::RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId,
std::string newName)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_newName(std::move(newName))
, m_description("Rename Render Layer")
{
if (!mgr) return;
for (const auto& info : mgr->GetRenderLayers()) {
if (!info.isDefault && info.layerIdentifier == m_layerId) {
m_oldName = info.name;
m_description = "Rename Render Layer " + info.name + " -> " + m_newName;
break;
}
}
}
void RenderLayerRenameCommand::Execute() {
if (!m_mgr) return;
m_mgr->RenameRenderLayer(m_layerId, m_newName);
}
void RenderLayerRenameCommand::Undo() {
if (!m_mgr || m_oldName.empty()) return;
m_mgr->RenameRenderLayer(m_layerId, m_oldName);
}
// ─── RenderLayerActivateCommand ───────────────────────────────────────────
RenderLayerActivateCommand::RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId)
: m_mgr(mgr)
, m_targetLayerId(std::move(targetLayerId))
, m_description(m_targetLayerId.empty() ? "Activate Render Layer: Default"
: "Activate Render Layer")
{
if (mgr) m_priorLayerId = mgr->GetActiveRenderLayerId();
}
void RenderLayerActivateCommand::Execute() {
if (!m_mgr) return;
if (m_targetLayerId.empty()) m_mgr->ActivateDefaultLayer();
else m_mgr->ActivateRenderLayer(m_targetLayerId);
}
void RenderLayerActivateCommand::Undo() {
if (!m_mgr) return;
if (m_priorLayerId.empty()) m_mgr->ActivateDefaultLayer();
else m_mgr->ActivateRenderLayer(m_priorLayerId);
}
} // namespace UsdLayerManager
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "../CommandHistory.h"
#include "../RenderLayerManager.h"
#include <string>
namespace UsdLayerManager {
/// Undo removes the created layer via RenderLayerManager::DeleteRenderLayer.
class RenderLayerCreateCommand : public ICommand {
public:
RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
/// Valid after Execute() — the new layer's identifier, so the panel can
/// select it immediately.
const std::string& GetCreatedLayerId() const { return m_layerId; }
private:
RenderLayerManager* m_mgr;
std::string m_name;
std::string m_layerId;
std::string m_description;
};
/// The backing file is left on disk by RenderLayerManager::DeleteRenderLayer
/// (only the sublayer reference is removed), so Undo just re-inserts it.
class RenderLayerDeleteCommand : public ICommand {
public:
RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::string m_description;
};
class RenderLayerRenameCommand : public ICommand {
public:
RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId, std::string newName);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::string m_newName;
std::string m_oldName;
std::string m_description;
};
/// Switches the active render layer (or back to Default). RenderLayerManager
/// itself tracks the edit target to restore on returning to Default, so this
/// command only needs to remember which layer was active before the switch.
class RenderLayerActivateCommand : public ICommand {
public:
/// targetLayerId empty = activate Default.
RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_targetLayerId; // "" = Default
std::string m_priorLayerId; // captured at construction; "" = Default
std::string m_description;
};
} // namespace UsdLayerManager
@@ -0,0 +1,57 @@
#include "RenderLayerMembershipCommand.h"
namespace UsdLayerManager {
// ─── RenderLayerAddMembersCommand ─────────────────────────────────────────
RenderLayerAddMembersCommand::RenderLayerAddMembersCommand(RenderLayerManager* mgr,
std::string layerId,
std::vector<pxr::SdfPath> paths)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_paths(std::move(paths))
, m_description(m_paths.size() == 1
? ("Add " + m_paths.front().GetString() + " to Render Layer")
: ("Add " + std::to_string(m_paths.size()) + " Members to Render Layer"))
{
}
void RenderLayerAddMembersCommand::Execute() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->AddMember(m_layerId, p);
}
void RenderLayerAddMembersCommand::Undo() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->RemoveMember(m_layerId, p);
}
// ─── RenderLayerRemoveMembersCommand ──────────────────────────────────────
RenderLayerRemoveMembersCommand::RenderLayerRemoveMembersCommand(RenderLayerManager* mgr,
std::string layerId,
std::vector<pxr::SdfPath> paths)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_paths(std::move(paths))
, m_description(m_paths.size() == 1
? ("Remove " + m_paths.front().GetString() + " from Render Layer")
: ("Remove " + std::to_string(m_paths.size()) + " Members from Render Layer"))
{
}
void RenderLayerRemoveMembersCommand::Execute() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->RemoveMember(m_layerId, p);
}
void RenderLayerRemoveMembersCommand::Undo() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->AddMember(m_layerId, p);
}
} // namespace UsdLayerManager
@@ -0,0 +1,44 @@
#pragma once
#include "../CommandHistory.h"
#include "../RenderLayerManager.h"
#include <pxr/usd/sdf/path.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
/// Adds one or more prim paths to a render layer's membership (Sdf-level, so
/// this works whether or not layerId is currently active). Re-runs viewport
/// isolation if layerId happens to be the active layer.
class RenderLayerAddMembersCommand : public ICommand {
public:
RenderLayerAddMembersCommand(RenderLayerManager* mgr, std::string layerId,
std::vector<pxr::SdfPath> paths);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::vector<pxr::SdfPath> m_paths;
std::string m_description;
};
class RenderLayerRemoveMembersCommand : public ICommand {
public:
RenderLayerRemoveMembersCommand(RenderLayerManager* mgr, std::string layerId,
std::vector<pxr::SdfPath> paths);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::vector<pxr::SdfPath> m_paths;
std::string m_description;
};
} // namespace UsdLayerManager
+79
View File
@@ -0,0 +1,79 @@
#include "ReparentPrimCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/namespaceEditor.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/base/tf/token.h>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
ReparentPrimCommand::ReparentPrimCommand(UsdStageRefPtr stage,
const SdfPath& srcPath,
const SdfPath& dstParentPath,
const std::string& finalName)
: m_stage(stage)
, m_srcPath(srcPath)
, m_origParentPath(srcPath.GetParentPath())
, m_origName(srcPath.GetName())
, m_dstParentPath(dstParentPath)
, m_finalName(finalName)
, m_description("Reparent " + srcPath.GetString() + " \xe2\x86\x92 " +
dstParentPath.GetString() + "/" + finalName)
{
m_actualNewPath = dstParentPath.AppendChild(TfToken(finalName));
}
static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) {
if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath())
return stage->GetPseudoRoot();
return stage->GetPrimAtPath(parentPath);
}
void ReparentPrimCommand::Execute() {
UsdPrim src = m_stage->GetPrimAtPath(m_srcPath);
if (!src.IsValid()) {
LOG_ERROR("ReparentPrimCommand::Execute: source prim not found: " + m_srcPath.GetString());
return;
}
UsdPrim dst = GetParentPrim(m_stage, m_dstParentPath);
if (!dst.IsValid()) {
LOG_ERROR("ReparentPrimCommand::Execute: destination parent not found: " + m_dstParentPath.GetString());
return;
}
UsdNamespaceEditor editor(m_stage);
editor.ReparentPrim(src, dst, TfToken(m_finalName));
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("ReparentPrimCommand::Execute: cannot reparent " + m_srcPath.GetString() + ": " + whyNot);
return;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("ReparentPrimCommand::Execute: ApplyEdits failed");
}
}
void ReparentPrimCommand::Undo() {
UsdPrim moved = m_stage->GetPrimAtPath(m_actualNewPath);
if (!moved.IsValid()) {
LOG_ERROR("ReparentPrimCommand::Undo: moved prim not found: " + m_actualNewPath.GetString());
return;
}
UsdPrim origParent = GetParentPrim(m_stage, m_origParentPath);
if (!origParent.IsValid()) {
LOG_ERROR("ReparentPrimCommand::Undo: original parent not found: " + m_origParentPath.GetString());
return;
}
UsdNamespaceEditor editor(m_stage);
editor.ReparentPrim(moved, origParent, TfToken(m_origName));
std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("ReparentPrimCommand::Undo: cannot reparent back " + m_actualNewPath.GetString() + ": " + whyNot);
return;
}
if (!editor.ApplyEdits()) {
LOG_ERROR("ReparentPrimCommand::Undo: ApplyEdits failed");
}
}
} // namespace UsdLayerManager
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <string>
namespace UsdLayerManager {
/// Reparents a USD prim under a new parent via UsdNamespaceEditor.
/// Supports undo by reparenting back to the original parent with the original name.
/// Pass SdfPath("/") as dstParentPath to reparent to the stage root.
class ReparentPrimCommand : public ICommand {
public:
ReparentPrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& srcPath,
const pxr::SdfPath& dstParentPath,
const std::string& finalName);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_srcPath; ///< original path before reparent
pxr::SdfPath m_origParentPath; ///< parent path before reparent
std::string m_origName; ///< prim name before reparent
pxr::SdfPath m_dstParentPath; ///< target parent path
std::string m_finalName; ///< name inside new parent (collision-free)
pxr::SdfPath m_actualNewPath; ///< full path after Execute (set in Execute)
std::string m_description;
};
} // namespace UsdLayerManager
+101 -3
View File
@@ -1,9 +1,11 @@
#include "ui/Application.h" #include "ui/Application.h"
#include "utils/Logger.h" #include "utils/Logger.h"
#include <pxr/base/plug/registry.h> #include <pxr/base/plug/registry.h>
#include <cstdio>
#include <exception> #include <exception>
#include <Windows.h> #include <Windows.h>
#include <string> #include <string>
#include <filesystem>
static void SetUsdPluginPath() { static void SetUsdPluginPath() {
char exePath[MAX_PATH]; char exePath[MAX_PATH];
@@ -17,6 +19,45 @@ static void SetUsdPluginPath() {
std::string pluginPath = exeDir + "\\usd"; std::string pluginPath = exeDir + "\\usd";
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str()); SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
// --- MaterialX / Arnold OSL include path -------------------------------
// hdArnold compiles MaterialX shaders by generating OSL whose first line is
// #include "mx_funcs.h"
// and compiling it as an "osl" node's `code` parameter. Without an include
// path that compile fails with:
// [osl] error: <buffer>:2:10: fatal error: 'mx_funcs.h' file not found
//
// The knob is Arnold's options.osl_includepath, which hdArnold exposes as the
// HDARNOLD_osl_includepath env setting (render_delegate/config.cpp).
//
// THIS MUST BE SET BEFORE ANY PLUGIN DLL IS LOADED. TF_DEFINE_ENV_SETTING
// registers a TF_REGISTRY_FUNCTION that eagerly calls TfGetEnvSetting() and
// caches the value permanently when hdArnold.dll loads -- which the
// LoadLibraryA pre-flight below does. Setting it later (e.g. from
// Application::Initialize) is silently ignored.
//
// Set via both the CRT (_putenv_s -> getenv) and the Win32 block
// (SetEnvironmentVariableA) since the two are not kept in sync on Windows.
{
namespace fs = std::filesystem;
const std::string mtlxLibs = exeDir + "\\libraries";
const std::string mtlxOslInclude = mtlxLibs + "\\stdlib\\genosl\\include";
if (fs::exists(mtlxOslInclude)) {
if (!getenv("HDARNOLD_osl_includepath")) {
_putenv_s("HDARNOLD_osl_includepath", mtlxOslInclude.c_str());
SetEnvironmentVariableA("HDARNOLD_osl_includepath",
mtlxOslInclude.c_str());
}
// Nodedef lookup (MATERIALX_NODE_DEFINITIONS). Separate knob from
// the OSL include path above -- it does NOT fix the include error.
if (!getenv("PXR_MTLX_STDLIB_SEARCH_PATHS")) {
_putenv_s("PXR_MTLX_STDLIB_SEARCH_PATHS", mtlxLibs.c_str());
SetEnvironmentVariableA("PXR_MTLX_STDLIB_SEARCH_PATHS",
mtlxLibs.c_str());
}
}
}
std::vector<std::string> pluginPaths; std::vector<std::string> pluginPaths;
WIN32_FIND_DATAA findData; WIN32_FIND_DATAA findData;
std::string searchPattern = pluginPath + "\\*"; std::string searchPattern = pluginPath + "\\*";
@@ -37,18 +78,75 @@ static void SetUsdPluginPath() {
FindClose(hFind); FindClose(hFind);
} }
if (!pluginPaths.empty()) { // Pre-flight: attempt to load each plugin DLL before registering it.
// This catches missing dependencies (e.g. sycl8.dll → Intel GPU drivers)
// gracefully and shows our own warning rather than a cryptic TF_ERROR.
std::vector<std::string> checkedPaths;
for (const auto& infoPath : pluginPaths) {
// Derive the DLL path from the plugInfo.json location:
// <plugin>/resources/plugInfo.json → ../<plugin>.dll
// e.g. usd/hdCycles/resources/plugInfo.json → usd/hdCycles.dll
size_t resPos = infoPath.rfind("\\resources\\plugInfo.json");
if (resPos != std::string::npos) {
std::string pluginDir = infoPath.substr(0, resPos);
size_t dirSlash = pluginDir.rfind('\\');
std::string pluginName = (dirSlash != std::string::npos)
? pluginDir.substr(dirSlash + 1) : pluginDir;
std::string dllPath = pluginPath + "\\" + pluginName + ".dll";
DWORD dllAttrs = GetFileAttributesA(dllPath.c_str());
if (dllAttrs != INVALID_FILE_ATTRIBUTES) {
// DLL file exists — verify it can actually be loaded.
// No LOAD_WITH_ALTERED_SEARCH_PATH: keep the application
// directory (exe dir, where embree4.dll etc. live) in the
// DLL search path so transitive deps resolve correctly.
HMODULE h = LoadLibraryA(dllPath.c_str());
if (!h) {
DWORD err = GetLastError();
char msg[256] = {};
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, err, 0, msg, static_cast<DWORD>(sizeof(msg) - 1), nullptr);
std::string errMsg(msg);
while (!errMsg.empty() && (errMsg.back() == '\r' || errMsg.back() == '\n'))
errMsg.pop_back();
char hexErr[12] = {};
sprintf_s(hexErr, "0x%08X", static_cast<unsigned>(err));
LOG_WARNING("Skipping plugin " + pluginName
+ " (DLL load failed " + hexErr + "): " + errMsg);
continue; // Skip registering this plugin
}
// Intentionally do NOT FreeLibrary here. Releasing the handle
// drops the refcount to 0 and unloads the DLL; when USD's plug
// system later loads it again DllMain re-fires, re-registering
// any TfEnvSettings and causing "duplicate definition" warnings.
// Keeping the handle loaded means USD's LoadLibrary is a no-op
// (refcount bump only, no DllMain call), preventing duplicates.
// The process holds these handles for its entire lifetime anyway.
}
}
checkedPaths.push_back(infoPath);
}
if (!checkedPaths.empty()) {
auto& registry = pxr::PlugRegistry::GetInstance(); auto& registry = pxr::PlugRegistry::GetInstance();
auto registered = registry.RegisterPlugins(pluginPaths); auto registered = registry.RegisterPlugins(checkedPaths);
LOG_INFO("Registered " + std::to_string(registered.size()) + " USD plugins from " + pluginPath); LOG_INFO("Registered " + std::to_string(registered.size()) + " USD plugins from " + pluginPath);
} else { } else {
LOG_WARNING("No USD plugins found at " + pluginPath); LOG_WARNING("No USD plugins found at " + pluginPath);
} }
} }
int main(int argc, char* argv[]) { int main(int /*argc*/, char* /*argv*/[]) {
try { try {
UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info); UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info);
// Temporary: capture LOG_INFO to a file so the [NodeGraph]/[ed] debug
// traces are readable after the fact (this is a WIN32-subsystem app,
// so stdout isn't visible when launched normally). Remove once the
// node-editor drag regression is diagnosed.
if (const char* appData = getenv("APPDATA")) {
std::filesystem::path logDir = std::filesystem::path(appData) / "UsdLayerManager";
std::filesystem::create_directories(logDir);
UsdLayerManager::Logger::Instance().SetLogFile((logDir / "debug.log").string());
}
LOG_INFO("=== USD Layer Manager Starting ==="); LOG_INFO("=== USD Layer Manager Starting ===");
+641 -4
View File
@@ -2,6 +2,7 @@
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/FileDialog.h" #include "../utils/FileDialog.h"
#include "../utils/PathUtils.h" #include "../utils/PathUtils.h"
#include "../utils/OcioConfigParser.h"
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/references.h> #include <pxr/usd/usd/references.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
@@ -10,9 +11,12 @@
#include <imgui_internal.h> #include <imgui_internal.h>
#include <vector> #include <vector>
#include <string> #include <string>
#include <fstream>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstring>
#include "../utils/MovieEncoder.h"
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -37,10 +41,13 @@ Application::Application()
: m_showDemoWindow(false) : m_showDemoWindow(false)
, m_showStageInfo(true) , m_showStageInfo(true)
, m_showStageEditor(true) , m_showStageEditor(true)
, m_showRenderLayerPanel(true)
, m_showSceneHierarchy(true) , m_showSceneHierarchy(true)
, m_showViewport(true) , m_showViewport(true)
, m_showPropertyPanel(true) , m_showPropertyPanel(true)
, m_showTimeline(true) , m_showTimeline(true)
, m_showCurveEditor(false)
, m_showMaterialEditor(false)
, m_running(false) { , m_running(false) {
} }
@@ -61,40 +68,76 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
// Create managers // Create managers
m_stageManager = std::make_unique<UsdStageManager>(); m_stageManager = std::make_unique<UsdStageManager>();
m_layerManager = std::make_unique<LayerManager>(); m_layerManager = std::make_unique<LayerManager>();
m_renderLayerManager = std::make_unique<RenderLayerManager>();
m_renderLayerManager->SetLayerManager(m_layerManager.get());
m_propertyManager = std::make_unique<PropertyManager>(); m_propertyManager = std::make_unique<PropertyManager>();
m_propertyManager->SetCommandHistory(&m_commandHistory); m_propertyManager->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel = std::make_unique<StageEditorPanel>(); m_stageEditorPanel = std::make_unique<StageEditorPanel>();
m_stageEditorPanel->SetLayerManager(m_layerManager.get()); m_stageEditorPanel->SetLayerManager(m_layerManager.get());
m_stageEditorPanel->SetCommandHistory(&m_commandHistory); m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>(); m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get()); m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory); m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get()); m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel = std::make_unique<RenderLayerPanel>();
m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel->SetCommandHistory(&m_commandHistory);
m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get());
m_viewportPanel = std::make_unique<ViewportPanel>(); m_viewportPanel = std::make_unique<ViewportPanel>();
m_viewportPanel->SetCommandHistory(&m_commandHistory); m_viewportPanel->SetCommandHistory(&m_commandHistory);
m_propertyPanel = std::make_unique<PropertyPanel>(); m_propertyPanel = std::make_unique<PropertyPanel>();
m_propertyPanel->SetPropertyManager(m_propertyManager.get()); m_propertyPanel->SetPropertyManager(m_propertyManager.get());
m_propertyPanel->SetCommandHistory(&m_commandHistory); m_propertyPanel->SetCommandHistory(&m_commandHistory);
m_curveEditorPanel = std::make_unique<CurveEditorPanel>();
m_curveEditorPanel->SetCommandHistory(&m_commandHistory);
m_materialManager = std::make_unique<MaterialManager>();
m_materialEditorPanel = std::make_unique<MaterialEditorPanel>();
m_materialEditorPanel->SetMaterialManager(m_materialManager.get());
m_materialEditorPanel->SetCommandHistory(&m_commandHistory);
m_timelinePanel = std::make_unique<TimelinePanel>(); m_timelinePanel = std::make_unique<TimelinePanel>();
m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime, m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime,
pxr::UsdTimeCode editTime) { pxr::UsdTimeCode editTime) {
m_viewportPanel->SetTimeCodes(displayTime, editTime); m_viewportPanel->SetTimeCodes(displayTime, editTime);
m_propertyPanel->SetTimeCodes(displayTime, editTime); m_propertyPanel->SetTimeCodes(displayTime, editTime);
m_curveEditorPanel->SetTimeCodes(displayTime, editTime);
};
m_curveEditorPanel->OnScrub = [this](double frame) {
m_timelinePanel->SetCurrentFrameExternal(frame);
};
m_timelinePanel->OnBrowseFolder = [this]() -> std::string {
HWND hwnd = m_imguiContext ? m_imguiContext->GetWindowHandle() : nullptr;
return FileDialog::BrowseFolder("Select Playblast Output Folder", hwnd);
}; };
// Initialize IconManager — must happen after OpenGL context is ready (ImGui init above). // Initialize IconManager — must happen after OpenGL context is ready (ImGui init above).
m_iconManager = std::make_unique<IconManager>(); m_iconManager = std::make_unique<IconManager>();
m_iconManager->Initialize(ResourcePath("resources/icons"), 24); m_iconManager->Initialize(ResourcePath("resources/icons"), 24);
m_sceneHierarchyPanel->SetIconManager(m_iconManager.get()); m_sceneHierarchyPanel->SetIconManager(m_iconManager.get());
m_propertyPanel->SetIconManager(m_iconManager.get());
m_viewportPanel->SetIconManager(m_iconManager.get()); m_viewportPanel->SetIconManager(m_iconManager.get());
m_timelinePanel->SetIconManager(m_iconManager.get()); m_timelinePanel->SetIconManager(m_iconManager.get());
m_stageEditorPanel->SetIconManager(m_iconManager.get()); m_stageEditorPanel->SetIconManager(m_iconManager.get());
m_materialEditorPanel->SetIconManager(m_iconManager.get());
m_renderLayerPanel->SetIconManager(m_iconManager.get());
m_sceneHierarchyPanel->SetOnPrimSelected( m_sceneHierarchyPanel->SetOnPrimSelected(
[this](const std::string& path) { [this](const std::string& path) {
m_viewportPanel->SetSelectedPrimPath(path); m_viewportPanel->SetSelectedPrimPath(path);
m_propertyPanel->SetSelectedPrimPath(path); m_propertyPanel->SetSelectedPrimPath(path);
m_curveEditorPanel->SetSelectedPrimPath(path);
m_materialEditorPanel->SetTargetPrimPath(path);
});
m_sceneHierarchyPanel->SetOnPrimsSelected(
[this](const std::vector<std::string>& paths) {
m_viewportPanel->SetSelectedPrimPaths(paths);
}); });
m_sceneHierarchyPanel->SetOnStageMetadataChanged( m_sceneHierarchyPanel->SetOnStageMetadataChanged(
@@ -102,24 +145,81 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
RefreshManagers(); RefreshManagers();
}); });
// Single click in viewport → sync hierarchy + property panel // Single click in viewport → sync hierarchy + property panel + curve editor
m_viewportPanel->OnPrimPicked = [this](const std::string& path) { m_viewportPanel->OnPrimPicked = [this](const std::string& path) {
m_sceneHierarchyPanel->SetSelectedPath(path); m_sceneHierarchyPanel->SetSelectedPath(path);
m_propertyPanel->SetSelectedPrimPath(path); m_propertyPanel->SetSelectedPrimPath(path);
m_curveEditorPanel->SetSelectedPrimPath(path);
m_materialEditorPanel->SetTargetPrimPath(path);
}; };
// Rect drag in viewport → sync hierarchy + property panel (primary path) // Rect drag in viewport → sync hierarchy + property panel + curve editor (primary)
m_viewportPanel->OnPrimsPickedRect = [this](const std::vector<std::string>& paths) { m_viewportPanel->OnPrimsPickedRect = [this](const std::vector<std::string>& paths) {
m_sceneHierarchyPanel->SetSelectedPaths(paths); m_sceneHierarchyPanel->SetSelectedPaths(paths);
m_propertyPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front()); m_propertyPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
m_curveEditorPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
m_materialEditorPanel->SetTargetPrimPath(paths.empty() ? "" : paths.front());
}; };
// "Edit Material" in the Property Panel's Material Binding section →
// open the Material Editor on the resolved material.
m_propertyPanel->OnEditMaterialRequested = [this](const std::string& materialPath) {
m_showMaterialEditor = true;
m_materialEditorPanel->OpenOrCreateMaterial(materialPath);
};
if (!m_stageManager->CreateInMemoryStage()) { if (!m_stageManager->CreateInMemoryStage()) {
LOG_ERROR("Failed to create default in-memory stage"); LOG_ERROR("Failed to create default in-memory stage");
} else { } else {
RefreshManagers(); RefreshManagers();
} }
// Set OCIO to bundled ACES 1.2 config if not already configured externally.
// Must happen before the first Render() call so Hydra picks up the env var.
if (!getenv("OCIO")) {
namespace fs = std::filesystem;
std::string ocioConfig = ResourcePath(
"resources/OpenColorIO-Configs/aces_1.2/config.ocio");
if (fs::exists(ocioConfig)) {
_putenv_s("OCIO", ocioConfig.c_str());
LOG_INFO("OCIO config: " + ocioConfig);
}
}
// NOTE: HDARNOLD_osl_includepath / PXR_MTLX_STDLIB_SEARCH_PATHS are set in
// main.cpp, NOT here. They must be in the environment before the plugin
// DLLs are loaded, because TF_DEFINE_ENV_SETTING caches the value when
// hdArnold.dll registers its settings. Setting them at this point is too
// late and is silently ignored.
if (const char* oslInc = getenv("HDARNOLD_osl_includepath")) {
LOG_INFO(std::string("Arnold OSL include path: ") + oslInc);
} else {
LOG_WARNING("HDARNOLD_osl_includepath not set — Arnold MaterialX "
"shaders will fail to compile ('mx_funcs.h' not found)");
}
// Load per-viewport settings and global preferences from AppData.
if (const char* appData = getenv("APPDATA")) {
namespace fs = std::filesystem;
fs::path dir = fs::path(appData) / "UsdLayerManager";
fs::create_directories(dir);
m_viewportSettingsPath = (dir / "viewport_settings.ini").string();
m_prefsPath = (dir / "preferences.ini").string();
m_viewportPanel->LoadSettings(m_viewportSettingsPath);
LoadPreferences();
ValidateOcioPreferences();
// Apply pref delegate only to tiles that have no saved delegate.
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
// Color correction is always global — apply to all tiles.
m_viewportPanel->ApplyGlobalColorCorrection(
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
m_materialEditorPanel->SetColorCorrectionFromPrefs(
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
}
LOG_INFO("Application initialized successfully"); LOG_INFO("Application initialized successfully");
return true; return true;
} }
@@ -137,11 +237,21 @@ void Application::Run() {
} }
void Application::Shutdown() { void Application::Shutdown() {
if (m_viewportPanel && !m_viewportSettingsPath.empty())
m_viewportPanel->SaveSettings(m_viewportSettingsPath);
if (!m_prefsPath.empty())
SavePreferences();
m_viewportPanel.reset(); m_viewportPanel.reset();
m_sceneHierarchyPanel.reset(); m_sceneHierarchyPanel.reset();
m_propertyPanel.reset(); m_propertyPanel.reset();
m_stageEditorPanel.reset(); m_stageEditorPanel.reset();
m_renderLayerPanel.reset();
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
// destroyed while the GL context still exists, like m_viewportPanel.
m_materialEditorPanel.reset();
m_propertyManager.reset(); m_propertyManager.reset();
m_renderLayerManager.reset();
m_layerManager.reset(); m_layerManager.reset();
if (m_iconManager) { if (m_iconManager) {
@@ -166,19 +276,35 @@ void Application::RefreshManagers() {
if (m_stageManager->HasStage()) { if (m_stageManager->HasStage()) {
auto stage = m_stageManager->GetStage(); auto stage = m_stageManager->GetStage();
m_layerManager->SetStage(stage); m_layerManager->SetStage(stage);
// After LayerManager (ActivateRenderLayer routes edit-target changes
// through it) but before RestorePersistedActiveLayer needs it.
m_renderLayerManager->SetStage(stage);
m_propertyManager->SetStage(stage); m_propertyManager->SetStage(stage);
m_sceneHierarchyPanel->SetStage(stage); m_sceneHierarchyPanel->SetStage(stage);
m_renderLayerPanel->SetStage(stage);
m_viewportPanel->SetStage(stage); m_viewportPanel->SetStage(stage);
m_viewportPanel->FrameScene(); m_viewportPanel->FrameScene();
m_propertyPanel->SetStage(stage); m_propertyPanel->SetStage(stage);
m_timelinePanel->SetStage(stage); m_timelinePanel->SetStage(stage);
m_curveEditorPanel->SetStage(stage);
m_materialManager->SetStage(stage);
m_materialEditorPanel->SetStage(stage);
// Re-applies whichever render layer was active last time this stage
// was saved (mute state itself isn't serialized — see
// RenderLayerManager::RestorePersistedActiveLayer).
m_renderLayerManager->RestorePersistedActiveLayer();
} else { } else {
m_layerManager->SetStage(nullptr); m_layerManager->SetStage(nullptr);
m_renderLayerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr); m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr); m_sceneHierarchyPanel->SetStage(nullptr);
m_renderLayerPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr); m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr); m_propertyPanel->SetStage(nullptr);
m_timelinePanel->SetStage(nullptr); m_timelinePanel->SetStage(nullptr);
m_curveEditorPanel->SetStage(nullptr);
m_materialManager->SetStage(nullptr);
m_materialEditorPanel->SetStage(nullptr);
} }
} }
@@ -224,6 +350,12 @@ void Application::RenderUI() {
ImGui::End(); ImGui::End();
} }
if (m_showRenderLayerPanel) {
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
m_renderLayerPanel->Render();
ImGui::End();
}
if (m_showViewport) { if (m_showViewport) {
m_viewportPanel->Render(&m_showViewport); m_viewportPanel->Render(&m_showViewport);
} }
@@ -249,6 +381,34 @@ void Application::RenderUI() {
ImGui::End(); ImGui::End();
} }
if (m_showCurveEditor) {
ImGui::SetNextWindowSize({960, 320}, ImGuiCond_FirstUseEver);
ImGui::Begin("Curve Editor", &m_showCurveEditor, ImGuiWindowFlags_NoCollapse);
m_curveEditorPanel->Render();
ImGui::End();
}
if (m_showMaterialEditor) {
ImGui::SetNextWindowSize({960, 320}, ImGuiCond_FirstUseEver);
ImGui::Begin("Material Editor", &m_showMaterialEditor, ImGuiWindowFlags_NoCollapse);
m_materialEditorPanel->Render();
ImGui::End();
}
if (m_showPreferences)
RenderPreferencesDialog();
// Capture happens after the timeline so that the "Start" button click in
// this frame is detected, and after the viewport has rendered the scene.
if (m_timelinePanel->IsPlayblasting()) {
CapturePlayblastFrame();
}
// If user aborted playblast mid-sequence, close any open encoder.
if (m_movieEncoder.IsOpen() && !m_timelinePanel->IsPlayblasting()) {
m_movieEncoder.Close();
}
m_imguiContext->Render(); m_imguiContext->Render();
} }
@@ -269,11 +429,10 @@ void Application::RenderStatusBar() {
if (open) { if (open) {
ImGui::BeginMenuBar(); ImGui::BeginMenuBar();
// FPS // FPS — right-aligned
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
char buf[32]; char buf[32];
snprintf(buf, sizeof(buf), "FPS: %.1f", io.Framerate); snprintf(buf, sizeof(buf), "FPS: %.1f", io.Framerate);
// Right-align: measure text, then position cursor.
float textW = ImGui::CalcTextSize(buf).x; float textW = ImGui::CalcTextSize(buf).x;
float avail = ImGui::GetContentRegionAvail().x; float avail = ImGui::GetContentRegionAvail().x;
if (avail > textW) if (avail > textW)
@@ -285,6 +444,344 @@ void Application::RenderStatusBar() {
ImGui::End(); ImGui::End();
} }
// ---------------------------------------------------------------------------
// Global preferences persistence
// ---------------------------------------------------------------------------
void Application::LoadPreferences()
{
std::ifstream f(m_prefsPath);
if (!f) return;
std::string line;
while (std::getline(f, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty() || line.front() == '#' || line.front() == '[') continue;
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "RenderDelegate") m_prefs.renderDelegate = val;
else if (key == "ColorCorrectionMode")
m_prefs.colorCorrectionMode = [&]{ try { return std::stoi(val); } catch(...){ return 1; } }();
else if (key == "OcioDisplay") m_prefs.ocioDisplay = val;
else if (key == "OcioView") m_prefs.ocioView = val;
else if (key == "OcioColorSpace") m_prefs.ocioColorSpace = val;
else if (key == "OcioLook") m_prefs.ocioLook = val;
else if (key == "MaterialBrowserWidth")
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
else if (key == "MaterialPreviewWidth")
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }();
else if (key == "KeepGraphNodeViewSettingsInUsd")
m_prefs.keepGraphNodeViewSettingsInUsd = (val == "1");
}
if (m_materialEditorPanel) {
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
m_prefs.materialPreviewWidth);
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd);
}
}
void Application::ValidateOcioPreferences()
{
// A persisted display/view that doesn't exist in the active OCIO config
// makes HdxColorCorrectionTask throw ("Display 'x' not found") every frame
// and silently skip correction, which reads as a washed-out viewport --
// worst on delegates like hdEmbree whose output depends on the transform.
// Note display and view are easy to invert: in the bundled ACES 1.2 config
// the only display is "ACES" and "sRGB" is one of its views.
const OcioConfig& cfg = GetCurrentOcioConfig();
if (!cfg.valid) {
// No $OCIO / unreadable config -- nothing to validate against. Leave
// the prefs alone rather than clobbering values that may be correct
// for a config supplied later.
return;
}
auto contains = [](const std::vector<std::string>& v, const std::string& s) {
return std::find(v.begin(), v.end(), s) != v.end();
};
// --- display ---
if (m_prefs.ocioDisplay.empty() || !contains(cfg.displays, m_prefs.ocioDisplay)) {
if (!m_prefs.ocioDisplay.empty()) {
LOG_WARNING("OCIO display '" + m_prefs.ocioDisplay
+ "' not in config; falling back to '"
+ cfg.defaultDisplay + "'");
}
m_prefs.ocioDisplay = cfg.defaultDisplay;
}
// --- view (must belong to the display resolved above) ---
auto viewsIt = cfg.views.find(m_prefs.ocioDisplay);
const std::vector<std::string>* views =
(viewsIt != cfg.views.end()) ? &viewsIt->second : nullptr;
if (views && !views->empty()) {
if (m_prefs.ocioView.empty() || !contains(*views, m_prefs.ocioView)) {
// Prefer the config default when it's valid for this display,
// otherwise take the display's first view.
const std::string fallback =
contains(*views, cfg.defaultView) ? cfg.defaultView : views->front();
if (!m_prefs.ocioView.empty()) {
LOG_WARNING("OCIO view '" + m_prefs.ocioView
+ "' not valid for display '" + m_prefs.ocioDisplay
+ "'; falling back to '" + fallback + "'");
}
m_prefs.ocioView = fallback;
}
}
// --- color space (same failure mode if it names a missing space) ---
if (!m_prefs.ocioColorSpace.empty()
&& !contains(cfg.colorSpaces, m_prefs.ocioColorSpace)) {
LOG_WARNING("OCIO color space '" + m_prefs.ocioColorSpace
+ "' not in config; clearing it");
m_prefs.ocioColorSpace.clear();
}
// --- look ---
if (!m_prefs.ocioLook.empty()
&& !contains(cfg.looks, m_prefs.ocioLook)) {
LOG_WARNING("OCIO look '" + m_prefs.ocioLook
+ "' not in config; clearing it");
m_prefs.ocioLook.clear();
}
LOG_INFO("OCIO display/view: '" + m_prefs.ocioDisplay + "' / '"
+ m_prefs.ocioView + "'");
}
void Application::SavePreferences()
{
// Pull the latest splitter positions; called from Shutdown before the
// panel is reset, and from the preferences dialog while it's alive.
if (m_materialEditorPanel)
m_materialEditorPanel->GetColumnWidths(m_prefs.materialBrowserWidth,
m_prefs.materialPreviewWidth);
std::ofstream f(m_prefsPath);
if (!f) return;
f << "[Preferences]\n";
f << "RenderDelegate=" << m_prefs.renderDelegate << "\n";
f << "ColorCorrectionMode=" << m_prefs.colorCorrectionMode << "\n";
f << "OcioDisplay=" << m_prefs.ocioDisplay << "\n";
f << "OcioView=" << m_prefs.ocioView << "\n";
f << "OcioColorSpace=" << m_prefs.ocioColorSpace << "\n";
f << "OcioLook=" << m_prefs.ocioLook << "\n";
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n";
}
void Application::ApplyPrefsToAllViewports()
{
if (!m_viewportPanel) return;
m_viewportPanel->ApplyGlobalColorCorrection(
m_prefs.colorCorrectionMode,
m_prefs.ocioDisplay,
m_prefs.ocioView,
m_prefs.ocioColorSpace,
m_prefs.ocioLook);
if (m_materialEditorPanel)
m_materialEditorPanel->SetColorCorrectionFromPrefs(
m_prefs.colorCorrectionMode,
m_prefs.ocioDisplay,
m_prefs.ocioView,
m_prefs.ocioColorSpace,
m_prefs.ocioLook);
}
void Application::RenderPreferencesDialog()
{
ImGui::SetNextWindowSize(ImVec2(520, 400), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
if (!ImGui::Begin("Preferences", &m_showPreferences,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking))
{
ImGui::End();
return;
}
bool ccChanged = false;
if (ImGui::BeginTabBar("PrefTabs")) {
// ── General ─────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("General")) {
ImGui::Spacing();
ImGui::TextDisabled("No general settings yet.");
ImGui::EndTabItem();
}
// ── Animation ───────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Animation")) {
ImGui::Spacing();
ImGui::TextDisabled("No animation settings yet.");
ImGui::EndTabItem();
}
// ── Material ────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Material")) {
ImGui::Spacing();
if (ImGui::Checkbox("Keep graph node view settings in USD customData",
&m_prefs.keepGraphNodeViewSettingsInUsd)) {
if (m_materialEditorPanel)
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(
m_prefs.keepGraphNodeViewSettingsInUsd);
}
ImGui::TextWrapped(
"When enabled, each node's pin display mode (all pins vs. "
"connected only) and any manually revealed pins are saved into "
"the material file and restored when it's reopened. When "
"disabled, this is a session-only view setting.");
ImGui::EndTabItem();
}
// ── Viewport ────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Viewport")) {
ImGui::Spacing();
// Render Delegate default — applied to viewports with no saved delegate on startup.
// Each viewport's toolbar controls its delegate independently after load.
ImGui::SeparatorText("Default Render Delegate");
ImGui::TextDisabled("Applied to viewports with no saved setting on startup.");
ImGui::Spacing();
auto plugins = UsdSceneRenderer::GetRendererPlugins();
pxr::TfToken curId(m_prefs.renderDelegate);
for (const auto& pluginId : plugins) {
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
if (name.empty()) name = pluginId.GetString();
bool sel = (pluginId == curId);
if (ImGui::RadioButton(name.c_str(), sel))
m_prefs.renderDelegate = pluginId.GetString();
}
ImGui::Spacing();
// Color Correction
ImGui::SeparatorText("Color Correction");
auto ccItem = [&](const char* label, int mode) {
if (ImGui::RadioButton(label, m_prefs.colorCorrectionMode == mode)) {
m_prefs.colorCorrectionMode = mode;
m_prefOcioFieldsSynced = false;
ccChanged = true;
}
};
ccItem("Disabled", 0);
ccItem("sRGB", 1);
ccItem("OpenColorIO", 2);
if (m_prefs.colorCorrectionMode == 2) {
ImGui::Spacing();
const OcioConfig& ocfg = GetCurrentOcioConfig();
if (!m_prefOcioFieldsSynced) {
std::string disp = m_prefs.ocioDisplay.empty() ? ocfg.defaultDisplay : m_prefs.ocioDisplay;
std::string view = m_prefs.ocioView.empty() ? ocfg.defaultView : m_prefs.ocioView;
strncpy(m_prefOcioDisplayBuf, disp.c_str(), 127);
strncpy(m_prefOcioViewBuf, view.c_str(), 127);
strncpy(m_prefOcioColorSpaceBuf, m_prefs.ocioColorSpace.c_str(), 127);
strncpy(m_prefOcioLookBuf, m_prefs.ocioLook.c_str(), 127);
if (m_prefs.ocioDisplay.empty()) m_prefs.ocioDisplay = disp;
if (m_prefs.ocioView.empty()) m_prefs.ocioView = view;
m_prefOcioFieldsSynced = true;
}
ImGui::PushItemWidth(280.f);
if (ImGui::BeginCombo("Display##pref", m_prefOcioDisplayBuf)) {
for (const auto& d : ocfg.displays) {
bool sel = (d == m_prefOcioDisplayBuf);
if (ImGui::Selectable(d.c_str(), sel)) {
strncpy(m_prefOcioDisplayBuf, d.c_str(), 127);
m_prefs.ocioDisplay = d;
auto vit = ocfg.views.find(d);
if (vit != ocfg.views.end() && !vit->second.empty()) {
strncpy(m_prefOcioViewBuf, vit->second[0].c_str(), 127);
m_prefs.ocioView = vit->second[0];
}
ccChanged = true;
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
{
static const std::vector<std::string> kEmpty;
auto vit = ocfg.views.find(std::string(m_prefOcioDisplayBuf));
const auto& views = (vit != ocfg.views.end()) ? vit->second : kEmpty;
if (ImGui::BeginCombo("View##pref", m_prefOcioViewBuf)) {
for (const auto& v : views) {
bool sel = (v == m_prefOcioViewBuf);
if (ImGui::Selectable(v.c_str(), sel)) {
strncpy(m_prefOcioViewBuf, v.c_str(), 127);
m_prefs.ocioView = v;
ccChanged = true;
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
if (ImGui::BeginCombo("Color Space##pref",
m_prefOcioColorSpaceBuf[0] ? m_prefOcioColorSpaceBuf : "(default)")) {
if (ImGui::Selectable("(default)", m_prefOcioColorSpaceBuf[0] == '\0')) {
m_prefOcioColorSpaceBuf[0] = '\0';
m_prefs.ocioColorSpace.clear();
ccChanged = true;
}
if (m_prefOcioColorSpaceBuf[0] == '\0') ImGui::SetItemDefaultFocus();
for (const auto& cs : ocfg.colorSpaces) {
bool sel = (cs == m_prefOcioColorSpaceBuf);
if (ImGui::Selectable(cs.c_str(), sel)) {
strncpy(m_prefOcioColorSpaceBuf, cs.c_str(), 127);
m_prefs.ocioColorSpace = cs;
ccChanged = true;
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::BeginCombo("Look##pref",
m_prefOcioLookBuf[0] ? m_prefOcioLookBuf : "(none)")) {
if (ImGui::Selectable("(none)", m_prefOcioLookBuf[0] == '\0')) {
m_prefOcioLookBuf[0] = '\0';
m_prefs.ocioLook.clear();
ccChanged = true;
}
if (m_prefOcioLookBuf[0] == '\0') ImGui::SetItemDefaultFocus();
for (const auto& look : ocfg.looks) {
bool sel = (look == m_prefOcioLookBuf);
if (ImGui::Selectable(look.c_str(), sel)) {
strncpy(m_prefOcioLookBuf, look.c_str(), 127);
m_prefs.ocioLook = look;
ccChanged = true;
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
if (ccChanged)
ApplyPrefsToAllViewports();
ImGui::End();
}
void Application::RenderMenuBar() { void Application::RenderMenuBar() {
if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) { if (ImGui::BeginMenu("File")) {
@@ -342,6 +839,10 @@ void Application::RenderMenuBar() {
m_commandHistory.Redo(); m_commandHistory.Redo();
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::Separator();
if (ImGui::MenuItem("Preferences..."))
m_showPreferences = true;
ImGui::EndMenu(); ImGui::EndMenu();
} }
} }
@@ -375,10 +876,13 @@ void Application::RenderMenuBar() {
if (ImGui::BeginMenu("View")) { if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor); ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy); ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
ImGui::MenuItem("Viewport", nullptr, &m_showViewport); ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel); ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
ImGui::MenuItem("Timeline", nullptr, &m_showTimeline); ImGui::MenuItem("Timeline", nullptr, &m_showTimeline);
ImGui::MenuItem("Curve Editor", nullptr, &m_showCurveEditor);
ImGui::MenuItem("Material Editor", nullptr, &m_showMaterialEditor);
ImGui::Separator(); ImGui::Separator();
ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo); ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo);
ImGui::MenuItem("Demo Window", nullptr, &m_showDemoWindow); ImGui::MenuItem("Demo Window", nullptr, &m_showDemoWindow);
@@ -468,7 +972,9 @@ void Application::SaveUsdFile() {
if (!m_stageManager->SaveStage()) { if (!m_stageManager->SaveStage()) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
return;
} }
SaveDirtyRenderLayers();
} }
void Application::SaveUsdFileAs() { void Application::SaveUsdFileAs() {
@@ -487,6 +993,11 @@ void Application::SaveUsdFileAs() {
if (!m_stageManager->SaveStageAs(filePath)) { if (!m_stageManager->SaveStageAs(filePath)) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
} else { } else {
// m_renderLayerManager's layer refs are identifier-keyed SdfLayer
// objects, independent of which UsdStage currently references
// them — safe to save before the stage swap RefreshManagers()
// below performs.
SaveDirtyRenderLayers();
// SaveStageAs reopens m_stageManager's stage from the new file path. // SaveStageAs reopens m_stageManager's stage from the new file path.
// RefreshManagers syncs m_layerManager (and others) to that new stage; // RefreshManagers syncs m_layerManager (and others) to that new stage;
// without this, subsequent sublayer edits go to the old (now stale) stage // without this, subsequent sublayer edits go to the old (now stale) stage
@@ -496,6 +1007,15 @@ void Application::SaveUsdFileAs() {
} }
} }
void Application::SaveDirtyRenderLayers() {
if (!m_renderLayerManager) return;
for (const auto& info : m_renderLayerManager->GetRenderLayers()) {
if (info.isDefault || !info.layer) continue;
if (info.layer->IsDirty())
info.layer->Save();
}
}
void Application::CloseUsdFile() { void Application::CloseUsdFile() {
m_stageManager->CloseStage(); m_stageManager->CloseStage();
// Re-create a fresh default stage so the app is always in an editable state. // Re-create a fresh default stage so the app is always in an editable state.
@@ -550,6 +1070,123 @@ void Application::AddReferenceToStage() {
} }
} }
// ---------------------------------------------------------------------------
// Writes top-down RGBA8 pixel data as a 24-bit BMP file.
// Negative height in the DIB header tells viewers the rows are top-down.
static bool WriteBMP(const char* path, int w, int h, const uint8_t* rgba) {
FILE* f = nullptr;
if (fopen_s(&f, path, "wb") != 0 || !f) return false;
int rowStride = (w * 3 + 3) & ~3;
int dataSize = rowStride * h;
int fileSize = 54 + dataSize;
uint8_t hdr[54] = {};
hdr[0] = 'B'; hdr[1] = 'M';
auto put32 = [&](int off, uint32_t v) { memcpy(hdr + off, &v, 4); };
auto put32s = [&](int off, int32_t v) { memcpy(hdr + off, &v, 4); };
auto put16 = [&](int off, uint16_t v) { memcpy(hdr + off, &v, 2); };
put32(2, uint32_t(fileSize));
put32(10, 54u);
put32(14, 40u);
put32s(18, w);
put32s(22, -h); // negative = top-down storage
put16(26, 1u);
put16(28, 24u);
put32(34, uint32_t(dataSize));
fwrite(hdr, 1, 54, f);
std::vector<uint8_t> row(rowStride, 0);
for (int y = 0; y < h; ++y) {
const uint8_t* src = rgba + y * w * 4;
for (int x = 0; x < w; ++x) {
row[x*3+0] = src[x*4+2]; // B
row[x*3+1] = src[x*4+1]; // G
row[x*3+2] = src[x*4+0]; // R
}
fwrite(row.data(), 1, rowStride, f);
}
fclose(f);
return true;
}
void Application::CapturePlayblastFrame() {
// The frame BeginPlayblast() is called, the viewport has already rendered
// at the old time. Skip one frame so the next viewport render picks up the
// start timecode before we read pixels.
if (m_timelinePanel->GetPlayblastSkipFirst()) {
m_timelinePanel->ClearPlayblastSkipFirst();
return;
}
const auto& settings = m_timelinePanel->GetPlayblastSettings();
auto& renderer = m_viewportPanel->GetRenderer();
// Re-render at the requested capture resolution. The renderer retains the
// camera matrices from the viewport render, and SetFraming() adapts the
// frustum to the new aspect ratio, so the output is correct regardless of
// whether it matches the display viewport size.
int cw = settings.captureWidth;
int ch = settings.captureHeight;
renderer.Render(cw, ch);
std::vector<uint8_t> pixels;
if (!renderer.CaptureFrame(pixels)) {
LOG_ERROR("Playblast: CaptureFrame failed");
m_timelinePanel->AbortPlayblast();
return;
}
int idx = m_timelinePanel->GetPlayblastFrameIndex();
std::error_code ec;
std::filesystem::create_directories(settings.outputDir, ec);
// ── Movie encode (direct libav streaming) ─────────────────────────────
if (settings.exportMovie) {
if (idx == 0) {
char moviePath[1024];
snprintf(moviePath, sizeof(moviePath), "%s\\%s.mp4",
settings.outputDir, settings.filePrefix);
if (!m_movieEncoder.Open(moviePath, cw, ch, settings.fps)) {
LOG_ERROR("MovieEncoder open failed: " + m_movieEncoder.GetLastError());
m_timelinePanel->AbortPlayblast();
return;
}
}
if (!m_movieEncoder.WriteFrame(pixels.data())) {
LOG_ERROR("MovieEncoder write failed: " + m_movieEncoder.GetLastError());
m_movieEncoder.Close();
m_timelinePanel->AbortPlayblast();
return;
}
}
// ── BMP frames (always written when not exporting movie, or when keepFrames) ─
if (!settings.exportMovie || settings.keepFrames) {
char filename[1024];
snprintf(filename, sizeof(filename), "%s\\%s.%04d.bmp",
settings.outputDir, settings.filePrefix, idx);
if (!WriteBMP(filename, cw, ch, pixels.data())) {
LOG_ERROR(std::string("Playblast: failed to write ") + filename);
if (settings.exportMovie) m_movieEncoder.Close();
m_timelinePanel->AbortPlayblast();
return;
}
}
if (!m_timelinePanel->AdvancePlayblast()) {
// Last frame — close the encoder (writes trailer).
if (settings.exportMovie && m_movieEncoder.IsOpen()) {
m_movieEncoder.Close();
LOG_INFO(std::string("Playblast movie: ") +
settings.outputDir + "\\" + settings.filePrefix + ".mp4");
}
LOG_INFO("Playblast complete: " + std::to_string(idx + 1) +
" frames captured to " + settings.outputDir);
}
}
void Application::CreatePrimOnStage(const std::string& typeName) { void Application::CreatePrimOnStage(const std::string& typeName) {
if (!m_stageManager->HasStage()) return; if (!m_stageManager->HasStage()) return;
+66 -3
View File
@@ -4,18 +4,41 @@
#include "IconManager.h" #include "IconManager.h"
#include "StageEditorPanel.h" #include "StageEditorPanel.h"
#include "SceneHierarchyPanel.h" #include "SceneHierarchyPanel.h"
#include "RenderLayerPanel.h"
#include "ViewportPanel.h" #include "ViewportPanel.h"
#include "PropertyPanel.h" #include "PropertyPanel.h"
#include "TimelinePanel.h" #include "TimelinePanel.h"
#include "CurveEditorPanel.h"
#include "MaterialEditorPanel.h"
#include "../core/UsdStageManager.h" #include "../core/UsdStageManager.h"
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/MaterialManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../utils/MovieEncoder.h"
#include <memory> #include <memory>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
/// Global (non-per-viewport) rendering preferences persisted to preferences.ini.
struct AppPreferences {
std::string renderDelegate = "HdStormRendererPlugin";
int colorCorrectionMode = 1; ///< ColorCorrectionMode cast to int (1 = sRGB)
std::string ocioDisplay;
std::string ocioView;
std::string ocioColorSpace;
std::string ocioLook;
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
/// When true, per-node pin display mode (Hypershade-style "show all" vs
/// "show connected only") and revealed hidden pins are authored into USD
/// customData so they survive reopening the material; when false, that
/// state is session-only.
bool keepGraphNodeViewSettingsInUsd = false;
};
class Application { class Application {
public: public:
Application(); Application();
@@ -35,34 +58,74 @@ private:
// File operations // File operations
void OpenUsdFile(); void OpenUsdFile();
void CreateNewUsdFile(); // creates fresh in-memory stage void CreateNewUsdFile();
void SaveUsdFile(); void SaveUsdFile();
void SaveUsdFileAs(); void SaveUsdFileAs();
void CloseUsdFile(); // closes file-backed stage, falls back to default stage void CloseUsdFile();
// Stage editing operations (also exposed via Stage menu) // Stage editing operations
void AddReferenceToStage(); void AddReferenceToStage();
void CreatePrimOnStage(const std::string& typeName); void CreatePrimOnStage(const std::string& typeName);
/// Explicitly saves every render-layer SdfLayer, since at most one is
/// ever in the stage's composition (unmuted) at a time and
/// UsdStage::Save() only walks layers currently in composition — the
/// rest would otherwise be silently dropped on save.
void SaveDirtyRenderLayers();
// Playblast
void CapturePlayblastFrame();
// Preferences
void LoadPreferences();
void SavePreferences();
/// Reconcile persisted OCIO display/view names against the active config.
/// Falls back to the config defaults when a name doesn't exist, so a stale
/// or mistyped preference can't silently disable color correction.
void ValidateOcioPreferences();
void ApplyPrefsToAllViewports();
void RenderPreferencesDialog();
std::unique_ptr<ImGuiContext> m_imguiContext; std::unique_ptr<ImGuiContext> m_imguiContext;
std::unique_ptr<IconManager> m_iconManager; std::unique_ptr<IconManager> m_iconManager;
std::unique_ptr<UsdStageManager> m_stageManager; std::unique_ptr<UsdStageManager> m_stageManager;
std::unique_ptr<LayerManager> m_layerManager; std::unique_ptr<LayerManager> m_layerManager;
std::unique_ptr<RenderLayerManager> m_renderLayerManager;
std::unique_ptr<PropertyManager> m_propertyManager; std::unique_ptr<PropertyManager> m_propertyManager;
std::unique_ptr<MaterialManager> m_materialManager;
CommandHistory m_commandHistory; CommandHistory m_commandHistory;
std::unique_ptr<StageEditorPanel> m_stageEditorPanel; std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel; std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
std::unique_ptr<RenderLayerPanel> m_renderLayerPanel;
std::unique_ptr<ViewportPanel> m_viewportPanel; std::unique_ptr<ViewportPanel> m_viewportPanel;
std::unique_ptr<PropertyPanel> m_propertyPanel; std::unique_ptr<PropertyPanel> m_propertyPanel;
std::unique_ptr<TimelinePanel> m_timelinePanel; std::unique_ptr<TimelinePanel> m_timelinePanel;
std::unique_ptr<CurveEditorPanel> m_curveEditorPanel;
std::unique_ptr<MaterialEditorPanel> m_materialEditorPanel;
bool m_showDemoWindow; bool m_showDemoWindow;
bool m_showStageInfo; bool m_showStageInfo;
bool m_showStageEditor; bool m_showStageEditor;
bool m_showRenderLayerPanel;
bool m_showSceneHierarchy; bool m_showSceneHierarchy;
bool m_showViewport; bool m_showViewport;
bool m_showPropertyPanel; bool m_showPropertyPanel;
bool m_showTimeline; bool m_showTimeline;
bool m_showCurveEditor;
bool m_showMaterialEditor;
bool m_running; bool m_running;
MovieEncoder m_movieEncoder;
std::string m_viewportSettingsPath;
std::string m_prefsPath;
AppPreferences m_prefs;
bool m_showPreferences = false;
// OCIO edit buffers for Preferences dialog
char m_prefOcioDisplayBuf[128] = {};
char m_prefOcioViewBuf[128] = {};
char m_prefOcioColorSpaceBuf[128] = {};
char m_prefOcioLookBuf[128] = {};
bool m_prefOcioFieldsSynced = false;
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
#pragma once
#include "../core/CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/usd/notice.h>
#include <pxr/base/tf/weakBase.h>
#include <pxr/base/tf/notice.h>
#include <imgui.h>
#include <functional>
#include <string>
#include <vector>
namespace UsdLayerManager {
class CurveEditorPanel : public pxr::TfWeakBase {
public:
void SetStage(pxr::UsdStageRefPtr stage);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetSelectedPrimPath(const std::string& path);
void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime);
void Render();
// Fires when the user drags the time cursor; drives the Timeline panel.
std::function<void(double frame)> OnScrub;
private:
// ── Internal data ────────────────────────────────────────────────────────
struct BezierKey {
double time = 0.0;
double value = 0.0;
// Tangent handles as (dt, dv) offsets from the key.
// in-handle is left of the key (dt < 0), out-handle is right (dt > 0).
double inTangentDt = -1.0, inTangentDv = 0.0;
double outTangentDt = 1.0, outTangentDv = 0.0;
bool tangentsBroken = false;
};
struct CurveChannel {
std::string displayName;
pxr::UsdAttribute attr;
int component = -1; // -1=scalar, 0/1/2/3 = X/Y/Z/W
ImVec4 color = {1,1,1,1};
bool visible = true;
bool dirty = false;
std::vector<double> origTimes;
std::vector<double> origValues;
std::vector<BezierKey> keys;
};
enum class HandlePart { Key, InTangent, OutTangent };
struct SelectedHandle { int chanIdx; int keyIdx; HandlePart part; };
// ── Methods ──────────────────────────────────────────────────────────────
void RefreshFromStage();
void FitCurveFromSamples(CurveChannel& ch);
void BakeChannelToUsd(CurveChannel& ch);
// Bezier math
double EvaluateBezier(const std::vector<BezierKey>& keys, double T) const;
void DrawBezierSegment(ImDrawList* dl, const BezierKey& k0, const BezierKey& k1,
ImVec4 color, ImVec2 canvasMin, ImVec2 canvasMax) const;
// Coordinate helpers
float TimeToX(double t, ImVec2 cMin, ImVec2 cMax) const;
float ValToY (double v, ImVec2 cMin, ImVec2 cMax) const;
double XToTime(float x, ImVec2 cMin, ImVec2 cMax) const;
double YToVal (float y, ImVec2 cMin, ImVec2 cMax) const;
ImVec2 KeyToScreen(const BezierKey& k, ImVec2 cMin, ImVec2 cMax) const;
// USD channel read/write helpers
double ReadChannelValue (const pxr::UsdAttribute& attr, int comp, double time) const;
void WriteChannelValue(const pxr::UsdAttribute& attr, int comp,
double time, double val) const;
void ClearAllTimeSamples(const pxr::UsdAttribute& attr) const;
// Rendering sub-sections
void RenderToolbar();
void RenderAttrList(float listWidth);
void RenderCanvas(ImVec2 canvasMin, ImVec2 canvasMax);
void RenderGrid(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
void RenderTimeCursor(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
void RenderCurve(ImDrawList* dl, CurveChannel& ch, ImVec2 cMin, ImVec2 cMax);
void HandleCanvasInput(ImVec2 cMin, ImVec2 cMax);
// Selection helpers
bool IsSelected(int chanIdx, int keyIdx, HandlePart part) const;
void Select(int chanIdx, int keyIdx, HandlePart part, bool additive);
void ClearSelection();
// Interaction helpers
void FrameAll();
void ApplyAllDirty();
void RevertAll();
void DeleteSelectedKeys();
void AddKeyAt(int chanIdx, double time, double value);
void RefitNeighborTangents(CurveChannel& ch, int keyIdx);
// Bezier metadata persistence (stored in prim customData["curveEditor"])
static std::string ChannelMetadataKey(const CurveChannel& ch);
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const CurveChannel& ch) const;
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const std::string& channelKey,
const std::vector<BezierKey>& keys) const;
bool LoadBezierFromMetadata(const pxr::UsdPrim& prim, CurveChannel& ch) const;
// ── State ────────────────────────────────────────────────────────────────
pxr::UsdStageRefPtr m_stage;
CommandHistory* m_commandHistory = nullptr;
std::string m_selectedPrimPath;
pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default();
pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default();
std::vector<CurveChannel> m_channels;
std::vector<SelectedHandle> m_selection;
int m_activeChannel = -1; // for double-click-to-add
bool m_needsRefresh = true;
bool m_suppressNotice = false; // set during bake writes to silence OnObjectsChanged
// USD change notification
pxr::TfNotice::Key m_stageChangeKey;
void OnObjectsChanged(const pxr::UsdNotice::ObjectsChanged& notice,
const pxr::UsdStageWeakPtr& sender);
// View range
double m_viewMinTime = 0.0, m_viewMaxTime = 100.0;
double m_viewMinVal = -1.0, m_viewMaxVal = 1.0;
// Pan/zoom drag state
bool m_isPanning = false;
ImVec2 m_panLastMouse = {0,0};
double m_panMinTimeStart = 0.0, m_panMaxTimeStart = 0.0;
double m_panMinValStart = 0.0, m_panMaxValStart = 0.0;
// Drag-move state
bool m_isDragging = false;
ImVec2 m_dragStartMouse= {0,0};
double m_dragStartTime = 0.0, m_dragStartValue = 0.0;
struct KeySnapshot { int chanIdx; int keyIdx; double time; double value;
double inDt, inDv, outDt, outDv; };
std::vector<KeySnapshot> m_dragSnapshot;
// Selection box drag
bool m_isBoxSelecting = false;
ImVec2 m_boxSelectStart = {0,0};
// Scrub cursor drag
bool m_isDraggingCursor = false;
// Options
bool m_simplifyOnBake = false;
float m_simplifyTol = 0.01f;
bool m_snapToFrame = true;
};
} // namespace UsdLayerManager
+5 -1
View File
@@ -60,6 +60,9 @@ static const char* IconFilename(Icon icon) {
case Icon::Refresh: return "arrows-rotate.svg"; case Icon::Refresh: return "arrows-rotate.svg";
case Icon::FilePlus: return "file-plus.svg"; case Icon::FilePlus: return "file-plus.svg";
case Icon::Pen: return "pen.svg"; case Icon::Pen: return "pen.svg";
case Icon::Settings: return "gear.svg";
case Icon::ChevronUp: return "chevron-up.svg";
case Icon::ChevronDown: return "chevron-down.svg";
default: return nullptr; default: return nullptr;
} }
} }
@@ -75,7 +78,8 @@ static constexpr Icon kAllIcons[] = {
Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad, Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad,
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause, Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce, Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
Icon::ChevronUp, Icon::ChevronDown,
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+4
View File
@@ -54,6 +54,10 @@ enum class Icon {
Refresh, // reload / refresh Refresh, // reload / refresh
FilePlus, // create new file FilePlus, // create new file
Pen, // edit / edit target Pen, // edit / edit target
Settings, // gear / viewport options
// List reordering
ChevronUp, // move item up
ChevronDown, // move item down
}; };
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as /// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
+25 -1
View File
@@ -5,6 +5,7 @@
#include <imgui.h> #include <imgui.h>
#include <imgui_impl_win32.h> #include <imgui_impl_win32.h>
#include <imgui_impl_opengl3.h> #include <imgui_impl_opengl3.h>
#include <filesystem>
// Forward declare message handler from imgui_impl_win32.cpp // Forward declare message handler from imgui_impl_win32.cpp
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
@@ -93,6 +94,29 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei
// With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any // With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any
// ImGui window is focused, which would block all viewport hotkeys (F, A, etc.). // ImGui window is focused, which would block all viewport hotkeys (F, A, etc.).
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Dock only while Shift is held during a title-bar/tab drag (the dock
// position overlay appears then); a plain drag just moves the window,
// so casual rearranging can't accidentally re-dock panels.
io.ConfigDockingWithShift = true;
// Windows move only via their title bar/tab, never by dragging empty
// content. Without this, a press-drag that a content widget doesn't
// capture falls through to ImGui's "drag body to move window" behaviour —
// which, in the node-editor canvas (whose background button doesn't
// reliably claim the drag in this nested-child embedding), meant dragging
// in the graph moved the whole Material Editor window. Title-bar dragging
// and Shift-to-dock are unaffected; this is also the desired behaviour for
// every other content area (viewports, timelines) where a content drag
// must never move the host window.
io.ConfigWindowsMoveFromTitleBarOnly = true;
// Store imgui.ini in %APPDATA%\UsdLayerManager\ so it doesn't litter the CWD.
if (const char* appData = getenv("APPDATA")) {
std::filesystem::path iniDir =
std::filesystem::path(appData) / "UsdLayerManager";
std::filesystem::create_directories(iniDir);
m_iniFilePath = (iniDir / "imgui.ini").string();
io.IniFilename = m_iniFilePath.c_str();
}
// Load Inter font. // Load Inter font.
// The build directory copies the font as Inter.ttc; the install step renames // The build directory copies the font as Inter.ttc; the install step renames
@@ -106,7 +130,7 @@ bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int hei
const char* tryPaths[] = { fontPath0.c_str(), fontPath1.c_str() }; const char* tryPaths[] = { fontPath0.c_str(), fontPath1.c_str() };
bool loaded = false; bool loaded = false;
for (const char* p : tryPaths) { for (const char* p : tryPaths) {
ImFont* f = io.Fonts->AddFontFromFileTTF(p, 16.0f, &fontConfig); ImFont* f = io.Fonts->AddFontFromFileTTF(p, 15.0f, &fontConfig);
if (f) { if (f) {
LOG_INFO(std::string("Loaded Inter font from ") + p); LOG_INFO(std::string("Loaded Inter font from ") + p);
loaded = true; loaded = true;
+1
View File
@@ -36,6 +36,7 @@ private:
bool m_shouldClose; bool m_shouldClose;
int m_width; int m_width;
int m_height; int m_height;
std::string m_iniFilePath; // persists for the lifetime of the ImGui context
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
File diff suppressed because it is too large Load Diff
+323
View File
@@ -0,0 +1,323 @@
#pragma once
#include "../core/CommandHistory.h"
#include "../core/MaterialManager.h"
#include "IconManager.h"
#include "MaterialPreviewRenderer.h"
#include "NodeThumbnailCache.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/base/vt/value.h>
#include <imgui_node_editor.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace UsdLayerManager {
/// Node-graph editor for authoring UsdShade material networks, with a
/// shader-ball preview rendered through Hydra.
class MaterialEditorPanel {
public:
MaterialEditorPanel();
~MaterialEditorPanel();
void SetStage(pxr::UsdStageRefPtr stage);
void SetMaterialManager(MaterialManager* mgr) { m_materialManager = mgr; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
/// Forwarded to the shader-ball preview so it matches the main viewport's
/// global color-correction settings.
void SetColorCorrectionFromPrefs(int ccMode, const std::string& ocioDisplay,
const std::string& ocioView,
const std::string& ocioColorSpace,
const std::string& ocioLook);
/// Opens the material at pathStr in the graph work area, creating it (as a
/// "Material" prim) if it doesn't exist yet. Also selects it in the browser.
void OpenOrCreateMaterial(const std::string& pathStr);
/// Tracks the current selection (mirrors PropertyPanel/ViewportPanel's
/// SetSelectedPrimPath) so the toolbar can offer "create + bind" / "bind"
/// affordances, and so selecting a prim with an already-bound material
/// auto-loads it into the canvas.
void SetTargetPrimPath(const std::string& path);
/// Splitter-adjustable column widths, persisted in preferences.ini by
/// Application. Setter clamps to the same range as the splitters.
void GetColumnWidths(float& browserWidth, float& previewWidth) const {
browserWidth = m_browserWidth;
previewWidth = m_previewWidth;
}
void SetColumnWidths(float browserWidth, float previewWidth);
/// Whether toggling a node's pin display mode / revealing a hidden pin
/// authors USD customData (persisted) or stays session-only. Pushed in by
/// Application from AppPreferences, live-updated when the user flips the
/// checkbox in Preferences -> Material.
void SetKeepGraphNodeViewSettingsInUsd(bool value) { m_keepGraphNodeViewSettingsInUsd = value; }
void Render();
private:
/// Resolved endpoint of a rendered pin, keyed by its ax::NodeEditor PinId.
struct PinInfo {
pxr::SdfPath nodePath;
std::string name;
bool isOutput = false;
pxr::SdfValueTypeName typeName;
/// True for the collapsed "more pins" connector nub rendered on a
/// node's title row when it's hiding unconnected pins (Hypershade
/// style) — name/typeName are unset. Connecting to/from it opens
/// RenderPinPickMenu() to choose which actual pin to wire, instead of
/// creating the connection immediately. See HandleCreateAndDelete().
bool isMeta = false;
};
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
struct LinkInfo {
pxr::SdfPath destNode;
std::string destInput;
};
/// Hypershade-style per-node pin display state. Always the render-time
/// source of truth (never resync-clobbered); optionally mirrored into USD
/// customData when m_keepGraphNodeViewSettingsInUsd is set. See
/// GetPinDisplayOverride()/IsPinVisible().
struct PinDisplayOverride {
PinDisplayMode mode = PinDisplayMode::All;
};
/// A connection dragged onto/from a collapsed meta pin, awaiting the user
/// picking which actual pin on nodePath/isOutput to wire up. otherPin is
/// the already-resolved endpoint on the far side of the link (itself meta
/// if the user dragged directly between two collapsed nubs, in which case
/// picking chains into a second RenderPinPickMenu() for that side).
struct PendingPinPick {
pxr::SdfPath nodePath;
bool isOutput = false;
PinInfo otherPin;
};
void RenderToolbar();
/// Property editor for the node selected in the graph, shown below the
/// shader-ball preview. Values apply live while a widget is being
/// dragged; one undoable command is pushed when the edit ends.
void RenderSelectedNodeProperties();
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
const pxr::UsdPrim& prim);
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
/// A "Color Space" row (raw/sRGB/auto/unset) under an Asset-typed input,
/// authoring the attribute's colorSpace metadata (UsdAttribute::
/// SetColorSpace) — the generic, renderer-agnostic mechanism Hydra reads
/// for texture color space (see UsdImagingDataSourceAttributeColorSpace),
/// and the only one available for shader types with no separate
/// sourceColorSpace input, e.g. MaterialX image nodes.
void RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
const pxr::UsdPrim& prim);
/// Hypershade-style browser: lists every material on the stage; the
/// selected one is loaded into the graph work area via "Show Graph"
/// (or double-click).
void RenderMaterialBrowser();
/// Creates a uniquely-named empty material under /Materials and opens it.
void CreateNewMaterial();
/// Preset graphs for the open material (one undoable command each):
/// UsdPreviewSurface + UsdUVTextures fed by an st primvar reader, or a
/// MaterialX standard_surface + image nodes fed by a texcoord node.
void CreateUsdPresetGraph();
void CreateMaterialXPresetGraph();
void CreateArnoldPresetGraph();
void RenderNodeGraphCanvas();
void RenderPreviewPanel();
void HandleCreateAndDelete();
/// Nuke-style TAB popup: type-to-filter shader node list; Enter or click
/// creates the highlighted node at canvasPos. When linkSource is non-null
/// (a connection was dragged onto empty canvas), the list is filtered to
/// nodes with a type-connectable pin and the chosen node is auto-wired to
/// linkSource.
void RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource = nullptr);
/// Creates the node and returns its prim path (empty on failure).
pxr::SdfPath CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos);
/// Nudges a creation point right until the new node's estimated rectangle
/// clears every existing node's rendered rectangle. Overlapping nodes
/// fight over the editor's hit test: the buried one becomes unmovable and
/// its pins unhoverable. Must be called with the editor context current.
ImVec2 FindFreeCanvasSpot(ImVec2 desired, const std::string& shaderId) const;
/// Binds an existing material to targetPath, undoably, restoring whatever
/// direct binding (if any) targetPath had before.
void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath);
/// Creates a new material named after m_targetPrimPath under /Materials,
/// binds it there, and opens it in the canvas.
void CreateAndBindMaterialForTarget();
void CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput);
void DeleteNode(const pxr::SdfPath& nodePath);
void DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput);
/// Resolved on-disk path of a texture node's image (its Asset-typed `file`
/// input), or empty if unauthored/unresolvable. Feeds the thumbnail cache.
std::string ResolveTextureFilePath(const ShaderGraphNode& node) const;
void SyncFromUsd();
void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
/// Seeds m_pinDisplayOverrides for any node path not already present
/// (i.e. not touched yet this session) from the just-synced m_graph's
/// pinDisplayMode (customData-backed). Never overwrites an existing
/// entry, so in-session toggles survive resyncs.
void SeedPinDisplayOverrides();
/// Creates the entry (defaulted to All) on first access.
PinDisplayOverride& GetPinDisplayOverride(const pxr::SdfPath& nodePath);
/// True if pin should be drawn this frame: All mode shows everything;
/// Connected mode shows only linked pins (linked pins are never hidden —
/// Link() unconditionally references pin IDs that must have been drawn —
/// so a hidden pin becomes visible the moment it's connected, via
/// RenderPinPickMenu()); Collapsed mode hides every real pin, linked or
/// not, in favor of the single connector nub per side (see
/// EffectivePinId(), which redirects existing links to that nub).
bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const;
/// Cycles a node's display mode: All -> Connected -> Collapsed -> All.
void CyclePinDisplayMode(const pxr::SdfPath& nodePath);
/// Sets every node currently in m_graph to mode (toolbar "set all" button).
void ApplyPinDisplayModeToAllNodes(PinDisplayMode mode);
/// Authors the node's current PinDisplayOverride into USD customData via
/// an undoable command, mirroring PersistNodePosition. No-op unless
/// m_keepGraphNodeViewSettingsInUsd is set.
void PersistPinDisplayState(const pxr::SdfPath& nodePath);
/// PinId to use when drawing a link touching (nodePath, pinName,
/// isOutput): the pin's own id normally, or that node's collapsed meta
/// nub id when the node is in Collapsed mode (so existing links visually
/// converge on the nub instead of referencing an undrawn real pin).
uintptr_t EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
/// Renders the deferred "choose pin" popup for m_pendingPinPick (Maya
/// Hypershade style): a connection was dragged onto/from a collapsed meta
/// pin, and the user now picks which actual attribute it wires to. Must
/// be called from within the NE::Suspend()/Resume() block, like the
/// other popups.
void RenderPinPickMenu();
/// Finishes a pick: creates the real connection to/from chosen, unless
/// the far side of the pending pick was itself a meta pin, in which case
/// this chains into a second pick for that side instead.
void CompletePinPick(const PinInfo& chosen);
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
const char* data, size_t size,
ax::NodeEditor::SaveReasonFlags reason,
void* userPointer);
ax::NodeEditor::EditorContext* m_editorContext = nullptr;
pxr::UsdStageRefPtr m_stage;
MaterialManager* m_materialManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr;
/// Last color correction pushed via SetColorCorrectionFromPrefs, cached
/// so RenderNodeGraphCanvas can pass it to NodeThumbnailCache every frame
/// (keeps in-canvas node thumbnails matching m_preview/the app global).
int m_ccMode = 1; // ColorCorrectionMode cast to int (1 = sRGB), mirrors AppPreferences' default
std::string m_ocioDisplay;
std::string m_ocioView;
std::string m_ocioColorSpace;
std::string m_ocioLook;
/// Material currently shown in the graph work area (empty = none).
pxr::SdfPath m_materialPath;
/// Material highlighted in the browser list; becomes m_materialPath when
/// the user clicks "Show Graph" (or double-clicks the entry).
pxr::SdfPath m_browserSelection;
/// Currently-selected prim, tracked for the "create + bind" / "bind"
/// toolbar affordances; empty when nothing (or a non-prim path) is selected.
pxr::SdfPath m_targetPrimPath;
ShaderGraphSnapshot m_graph;
/// NodeId (uintptr_t) -> prim path, for nodes already known to the editor
/// this session (seeded position, so re-syncing won't fight live drags).
std::unordered_map<uintptr_t, pxr::SdfPath> m_nodeIdToPath;
/// Rebuilt every frame from the current m_graph while rendering.
std::unordered_map<uintptr_t, PinInfo> m_pinIdToInfo;
std::unordered_map<uintptr_t, LinkInfo> m_linkIdToInfo;
MaterialPreviewRenderer m_preview;
/// Per-node in-canvas thumbnails (texture images + material shader balls).
NodeThumbnailCache m_thumbnails;
ImVec2 m_pendingCreateNodePos{0.0f, 0.0f};
/// Alt+RMB drag distance not yet converted into a discrete wheel-zoom
/// step (the node editor only zooms in wheel increments).
float m_zoomDragAccum = 0.0f;
char m_nodeSearchBuf[128] = "";
int m_nodeSearchSelected = 0;
/// Nuke-style drag-to-create: set when a connection was dragged from a pin
/// onto empty canvas. m_openLinkDragMenu requests the create-node popup on
/// the next frame (deferred so OpenPopup runs while the editor is
/// suspended); m_linkDragSourcePin is the pin to auto-wire the new node to.
bool m_openLinkDragMenu = false;
PinInfo m_linkDragSourcePin;
/// Hypershade-style pin display state, keyed by node path string. Always
/// the render-time source of truth; see PinDisplayOverride.
std::unordered_map<std::string, PinDisplayOverride> m_pinDisplayOverrides;
/// Whether node toggles author USD customData (see
/// SetKeepGraphNodeViewSettingsInUsd).
bool m_keepGraphNodeViewSettingsInUsd = false;
/// Deferred "choose pin" popup request: set from HandleCreateAndDelete()
/// when a link is dropped on/dragged from a collapsed meta pin, actually
/// opened after NE::Suspend(), like m_openLinkDragMenu.
bool m_openPinPickMenu = false;
PendingPinPick m_pendingPinPick;
/// Toolbar toggle: whether node thumbnails (texture images / material
/// shader-ball renders) are shown at all. Off skips the reserved space
/// and the decode/render work entirely, not just the visual.
bool m_showNodeThumbnails = true;
/// Toolbar "set all nodes" pin-display button: the mode last broadcast
/// to every node (and the icon/tooltip shown), independent of what any
/// individual node is actually in after the user tweaks one by hand.
PinDisplayMode m_toolbarPinDisplayMode = PinDisplayMode::All;
/// Column widths of the browser (left) and preview (right) sections,
/// user-adjustable via the splitters between them and the canvas.
float m_browserWidth = 220.0f;
float m_previewWidth = 320.0f;
/// Fraction of the browser column's height given to the materials list;
/// the create-node list takes the rest. User-adjustable via the horizontal
/// splitter between them.
float m_browserListRatio = 0.45f;
/// Inline-rename state for the materials list (double-click a row to start;
/// mirrors SceneHierarchyPanel): the material being renamed (empty = none),
/// the edit buffer, and a one-frame flag to focus the InputText on start.
pxr::SdfPath m_renamingMaterial;
char m_materialRenameBuf[128] = "";
bool m_materialRenameJustStarted = false;
/// Search text of the browser's persistent create-node list.
char m_browserNodeSearchBuf[128] = "";
/// Shader id clicked in the browser's create-node list; created at the
/// canvas view center on the next RenderNodeGraphCanvas (ScreenToCanvas
/// is only valid inside the editor's Begin/End).
std::string m_pendingCreateShaderId;
/// Debug event-trace state (see the logging block at the end of
/// RenderNodeGraphCanvas): whether the last LMB press landed on a node
/// rect, whether that press's drag-start has been logged yet, and the
/// editor NodeIds hit at press time (their live positions are logged on
/// every drag-move so a node that stops following the mouse is visible).
bool m_debugPressOnNode = false;
bool m_debugDragLogged = false;
std::vector<uintptr_t> m_debugDragNodeIds;
/// First node currently selected in the graph editor (empty = none);
/// drives the properties section under the preview.
pxr::SdfPath m_selectedNodePath;
/// Value/authored-state of the input being edited, captured when its
/// widget activates, so the commit command can restore it on undo. Only
/// one ImGui widget can be active at a time, so one slot suffices.
pxr::VtValue m_preEditValue;
bool m_preEditWasAuthored = false;
};
} // namespace UsdLayerManager
+628
View File
@@ -0,0 +1,628 @@
#include "MaterialPreviewRenderer.h"
#include "../utils/Logger.h"
#include "../utils/PathUtils.h"
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usdGeom/sphere.h>
#include <pxr/usd/usdGeom/cube.h>
#include <pxr/usd/usdGeom/cylinder.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdGeom/scope.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdVol/volume.h>
#include <pxr/usd/usdVol/openVDBAsset.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/lightAPI.h>
#include <pxr/usd/usdLux/tokens.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/primSpec.h>
#include <imgui.h>
#include <filesystem>
namespace UsdLayerManager {
namespace {
const pxr::SdfPath kShapesRootPath("/Preview/Shapes");
const pxr::SdfPath kDistantLightPath("/Preview/Light");
const pxr::SdfPath kDomeLightPath("/Preview/DomeLight");
/// Selectable preview geometry; prim name is a child of kShapesRootPath.
/// Shapes backed by an asset file are only created when the file exists.
/// frameFit: camera framing margin — the camera frames the shape's bounding
/// box, and a cube fills its box completely while a sphere only inscribes it,
/// so boxy shapes need a larger margin to read at a similar visual size.
struct ShapePreset {
const char* label;
const char* primName;
double frameFit;
};
const ShapePreset kShapePresets[] = {
{"Sphere", "Sphere", 1.6},
{"Cube", "Cube", 2.1},
{"Cylinder", "Cylinder", 1.8},
{"Teapot", "Teapot", 1.6},
{"Hair", "Hair", 1.6},
{"Cloud Volume", "Cloud", 1.6},
};
constexpr int kShapePresetCount = static_cast<int>(sizeof(kShapePresets) / sizeof(kShapePresets[0]));
/// Scratch UsdPreviewSurface authored inside the (referenced) material scope
/// when previewing a non-terminal node output — child of the material so the
/// connection stays encapsulated.
const pxr::TfToken kNodePreviewShaderName("__uslm_node_preview");
struct LightPreset {
const char* label;
const char* file; ///< under resources/hdri/ (CC0, Poly Haven; fetched by CMake)
};
const LightPreset kLightPresets[] = {
{"External", "kloofendal_48d_partly_cloudy_puresky_1k.exr"},
{"Room", "lebombo_1k.exr"},
{"Interior", "artist_workshop_1k.exr"},
{"Sunset", "venice_sunset_1k.exr"},
};
constexpr int kLightPresetCount = static_cast<int>(sizeof(kLightPresets) / sizeof(kLightPresets[0]));
/// Authors a polygonal UsdGeomMesh from prebuilt arrays: vertex-interpolated
/// normals + a `st` texCoord2f primvar, no subdivision, single-sided. Shared by
/// every preview shape so they all light and texture identically.
void DefineUvMesh(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path,
const pxr::VtVec3fArray& points, const pxr::VtVec3fArray& normals,
const pxr::VtVec2fArray& st, const pxr::VtIntArray& fvCounts,
const pxr::VtIntArray& fvIndices,
const pxr::GfVec3f& extentMin, const pxr::GfVec3f& extentMax) {
pxr::UsdGeomMesh mesh = pxr::UsdGeomMesh::Define(stage, path);
mesh.CreatePointsAttr(pxr::VtValue(points));
mesh.CreateNormalsAttr(pxr::VtValue(normals));
mesh.SetNormalsInterpolation(pxr::UsdGeomTokens->vertex);
mesh.CreateFaceVertexCountsAttr(pxr::VtValue(fvCounts));
mesh.CreateFaceVertexIndicesAttr(pxr::VtValue(fvIndices));
// Polygonal (not subdivided) so the authored smooth normals and UVs are used
// verbatim. Single-sided (the default): a closed shape never shows a
// backface, and double-sided shading runs a view-dependent normal flip that
// corrupts the mirror-reflection vector near the silhouette.
mesh.CreateSubdivisionSchemeAttr(pxr::VtValue(pxr::UsdGeomTokens->none));
mesh.CreateExtentAttr(pxr::VtValue(pxr::VtVec3fArray{extentMin, extentMax}));
pxr::UsdGeomPrimvarsAPI(mesh).CreatePrimvar(
pxr::TfToken("st"), pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->vertex)
.Set(st);
}
/// Authors a UV-mapped mesh sphere (Y-up) at `path` with vertex normals and a
/// `st` texCoord2f primvar. The implicit UsdGeomSphere carries no texture
/// coordinates (GeomUtilSphereMeshGenerator emits only points/normals/topology),
/// so a UsdUVTexture fed by a UsdPrimvarReader_float2("st") would only ever read
/// the reader's fallback and paint a flat colour. This mesh gives the shader
/// ball real UVs so textures map across every render delegate.
void CreateUvSphere(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path, double radius) {
constexpr int rings = 48; // latitude bands (theta 0..pi)
constexpr int sectors = 96; // longitude bands (phi 0..2pi)
constexpr double kPi = 3.14159265358979323846;
const float r = static_cast<float>(radius);
pxr::VtVec3fArray points, normals;
pxr::VtVec2fArray st;
points.reserve((rings + 1) * (sectors + 1));
normals.reserve((rings + 1) * (sectors + 1));
st.reserve((rings + 1) * (sectors + 1));
// A duplicated seam column (sectors+1 wide) gives distinct u=0 and u=1
// vertices so the texture wraps without a smear across the seam.
for (int i = 0; i <= rings; ++i) {
const float v = static_cast<float>(i) / rings;
const float th = v * static_cast<float>(kPi);
const float sinT = std::sin(th), cosT = std::cos(th);
for (int j = 0; j <= sectors; ++j) {
const float u = static_cast<float>(j) / sectors;
const float phi = u * 2.0f * static_cast<float>(kPi);
const pxr::GfVec3f n(sinT * std::cos(phi), cosT, sinT * std::sin(phi));
points.push_back(n * r);
normals.push_back(n);
st.push_back(pxr::GfVec2f(u, 1.0f - v)); // flip V so image top → sphere top
}
}
pxr::VtIntArray fvCounts, fvIndices;
const int stride = sectors + 1;
for (int i = 0; i < rings; ++i)
for (int j = 0; j < sectors; ++j) {
const int a = i * stride + j;
fvCounts.push_back(4);
// Wind CCW as seen from outside so the (single-sided) front face and
// the outward vertex normals agree; a reversed winding would either
// cull the visible surface or flip its shading normal.
fvIndices.push_back(a);
fvIndices.push_back(a + 1);
fvIndices.push_back(a + stride + 1);
fvIndices.push_back(a + stride);
}
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
pxr::GfVec3f(-r, -r, -r), pxr::GfVec3f(r, r, r));
}
/// UV-mapped box: 24 vertices (4 per face) so every face gets its own outward
/// normal and a full 0..1 UV square. halfSize is the half-edge length.
void CreateUvCube(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path, double halfSize) {
const float h = static_cast<float>(halfSize);
pxr::VtVec3fArray points, normals;
pxr::VtVec2fArray st;
pxr::VtIntArray fvCounts, fvIndices;
// p0..p3 are wound CCW as seen from outside, so the front face agrees with n.
auto addFace = [&](const pxr::GfVec3f& p0, const pxr::GfVec3f& p1,
const pxr::GfVec3f& p2, const pxr::GfVec3f& p3,
const pxr::GfVec3f& n) {
const int base = static_cast<int>(points.size());
points.push_back(p0); points.push_back(p1); points.push_back(p2); points.push_back(p3);
for (int k = 0; k < 4; ++k) normals.push_back(n);
st.push_back(pxr::GfVec2f(0, 0)); st.push_back(pxr::GfVec2f(1, 0));
st.push_back(pxr::GfVec2f(1, 1)); st.push_back(pxr::GfVec2f(0, 1));
fvCounts.push_back(4);
fvIndices.push_back(base + 0); fvIndices.push_back(base + 1);
fvIndices.push_back(base + 2); fvIndices.push_back(base + 3);
};
addFace({ h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h}, { 1, 0, 0}); // +X
addFace({-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h}, {-1, 0, 0}); // -X
addFace({-h, h,-h}, {-h, h, h}, { h, h, h}, { h, h,-h}, { 0, 1, 0}); // +Y
addFace({-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h}, { 0,-1, 0}); // -Y
addFace({-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, { 0, 0, 1}); // +Z
addFace({ h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h}, { 0, 0,-1}); // -Z
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
pxr::GfVec3f(-h, -h, -h), pxr::GfVec3f(h, h, h));
}
/// UV-mapped cylinder (Y axis): radial side wall + two end caps. Side normals
/// are radial, caps ±Y; UVs wrap the wall (u around, v along height) and map
/// each cap as a disk.
void CreateUvCylinder(const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& path,
double radius, double halfHeight) {
constexpr int sectors = 96;
constexpr double kPi = 3.14159265358979323846;
const float R = static_cast<float>(radius);
const float hy = static_cast<float>(halfHeight);
pxr::VtVec3fArray points, normals;
pxr::VtVec2fArray st;
pxr::VtIntArray fvCounts, fvIndices;
// Side wall: sectors+1 columns (duplicated seam for a clean u wrap), each a
// bottom+top pair interleaved so column j owns indices 2j (bottom), 2j+1 (top).
for (int j = 0; j <= sectors; ++j) {
const float u = static_cast<float>(j) / sectors;
const float phi = u * 2.0f * static_cast<float>(kPi);
const float c = std::cos(phi), s = std::sin(phi);
points.push_back(pxr::GfVec3f(R * c, -hy, R * s));
normals.push_back(pxr::GfVec3f(c, 0, s));
st.push_back(pxr::GfVec2f(u, 0));
points.push_back(pxr::GfVec3f(R * c, hy, R * s));
normals.push_back(pxr::GfVec3f(c, 0, s));
st.push_back(pxr::GfVec2f(u, 1));
}
for (int j = 0; j < sectors; ++j) {
const int b0 = 2 * j, t0 = 2 * j + 1, b1 = 2 * (j + 1), t1 = 2 * (j + 1) + 1;
fvCounts.push_back(4); // b_j, t_j, t_{j+1}, b_{j+1} → outward radial
fvIndices.push_back(b0); fvIndices.push_back(t0);
fvIndices.push_back(t1); fvIndices.push_back(b1);
}
// End caps as triangle fans around a center vertex; ring vertices carry the
// cap normal (not the radial one) and disk UVs.
auto addCap = [&](float y, float ny) {
const int center = static_cast<int>(points.size());
points.push_back(pxr::GfVec3f(0, y, 0));
normals.push_back(pxr::GfVec3f(0, ny, 0));
st.push_back(pxr::GfVec2f(0.5f, 0.5f));
const int ringBase = static_cast<int>(points.size());
for (int j = 0; j < sectors; ++j) {
const float phi = static_cast<float>(j) / sectors * 2.0f * static_cast<float>(kPi);
const float c = std::cos(phi), s = std::sin(phi);
points.push_back(pxr::GfVec3f(R * c, y, R * s));
normals.push_back(pxr::GfVec3f(0, ny, 0));
st.push_back(pxr::GfVec2f(0.5f + 0.5f * c, 0.5f + 0.5f * s));
}
for (int j = 0; j < sectors; ++j) {
const int a = ringBase + j, b = ringBase + (j + 1) % sectors;
fvCounts.push_back(3);
fvIndices.push_back(center);
// Wind so the tri normal matches the cap direction (+Y vs -Y).
if (ny > 0) { fvIndices.push_back(b); fvIndices.push_back(a); }
else { fvIndices.push_back(a); fvIndices.push_back(b); }
}
};
addCap( hy, 1.0f);
addCap(-hy, -1.0f);
DefineUvMesh(stage, path, points, normals, st, fvCounts, fvIndices,
pxr::GfVec3f(-R, -hy, -R), pxr::GfVec3f(R, hy, R));
}
} // namespace
void MaterialPreviewRenderer::EnsureInitialized() {
if (m_initialized) return;
m_previewStage = pxr::UsdStage::CreateInMemory();
// Everything authored here (sphere, lights, reference) must go to the
// root layer: the session layer is reserved for node-preview overrides
// and is wiped wholesale on every SetMaterial.
m_previewStage->SetEditTarget(pxr::UsdEditTarget(m_previewStage->GetRootLayer()));
// Pin the up axis rather than inheriting the site fallback: the light
// rig, dome-pole compensation, and camera framing all assume Y-up.
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
// All selectable preview shapes live under one scope; the material is
// bound on the scope so it inherits to whichever shape is active, and
// ApplyPreviewShape keeps exactly one child active.
pxr::UsdGeomScope::Define(m_previewStage, kShapesRootPath);
// UV-mapped meshes (not implicit UsdGeom prims) so textured materials —
// e.g. a UsdUVTexture wired into diffuseColor via a UsdPrimvarReader "st" —
// map onto the preview geometry instead of sampling a single texel. The
// implicit primitives generate no `st` primvar, so they can't show textures.
CreateUvSphere(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Sphere")), 1.0);
CreateUvCube(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cube")), 0.6);
CreateUvCylinder(m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cylinder")), 0.7, 0.8);
// Asset-backed shapes, created only when their file is bundled.
for (const char* name : {"Teapot", "Hair"}) {
const std::string file = ResourcePath(
std::string("resources/preview/") + pxr::TfStringToLower(name) + ".usdc");
if (!std::filesystem::exists(file)) continue;
pxr::UsdPrim prim = m_previewStage->DefinePrim(
kShapesRootPath.AppendChild(pxr::TfToken(name)));
prim.GetReferences().AddReference(file);
}
const std::string vdbFile = ResourcePath("resources/vdb/bunny_cloud.vdb");
if (std::filesystem::exists(vdbFile)) {
pxr::UsdVolVolume cloud = pxr::UsdVolVolume::Define(
m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cloud")));
pxr::UsdVolOpenVDBAsset density = pxr::UsdVolOpenVDBAsset::Define(
m_previewStage, cloud.GetPath().AppendChild(pxr::TfToken("density")));
density.CreateFilePathAttr().Set(pxr::SdfAssetPath(vdbFile));
density.CreateFieldNameAttr().Set(pxr::TfToken("density"));
cloud.CreateFieldRelationship(pxr::TfToken("density"), density.GetPath());
}
// Storm renders fine with zero authored lights (it injects a GL headlight
// as a fallback), but proper Hydra delegates like Embree/Arnold shade
// through scene lights only — without one they render solid black.
// This distant light is the fallback when the HDR dome preset's texture
// isn't bundled; ApplyLightPreset toggles between it and the dome.
pxr::UsdLuxDistantLight light = pxr::UsdLuxDistantLight::Define(m_previewStage, kDistantLightPath);
// pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(15000.0f));
pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(1500.0f));
// Default distant-light orientation shines along local -Z, which lights
// the far side of the sphere from the camera's default view. Angle it
// like a classic front-upper-left key light instead. Adding this scene
// light suppresses UsdSceneRenderer's GL headlight fallback (it only
// kicks in when the stage has no authored lights), so this needs to
// carry the whole preview on its own.
pxr::UsdGeomXformCommonAPI(light.GetPrim()).SetRotate(pxr::GfVec3f(-35.0f, -35.0f, 0.0f));
m_camera.SetStage(m_previewStage);
m_renderer.SetStage(m_previewStage);
m_renderer.SetShowGrid(false);
// Raised refinement so the implicit sphere tessellates smoothly instead
// of showing faceted silhouettes at swatch scale.
m_renderer.SetComplexity(1.3f);
ApplyLightPreset(m_lightPreset);
ApplyPreviewShape(m_previewShape); // activates one shape + frames the camera
// Maya-style default 3/4 view: orbit the framed camera ~45° around and
// ~25° down instead of the dead-on front view (subsequent shape switches
// keep whatever orientation the user has orbited to).
m_camera.Tumble(-45.0, 25.0);
m_renderer.SetCameraStateFromGfCamera(
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
m_initialized = true;
m_dirty = true;
}
void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
const pxr::SdfPath& materialPath,
size_t graphRevision,
const pxr::SdfPath& previewNodePath,
const std::string& previewNodeOutput,
bool previewOutputIsTerminal) {
EnsureInitialized();
if (!sourceStage || materialPath.IsEmpty()) return;
if (materialPath == m_materialPath && graphRevision == m_lastGraphRevision &&
previewNodePath == m_previewNodePath && previewNodeOutput == m_previewNodeOutput)
return; // nothing the shader ball cares about has changed
if (!sourceStage->GetPrimAtPath(materialPath)) {
LOG_WARNING("MaterialPreviewRenderer: no prim at " + materialPath.GetString());
return;
}
// Compose the material into the preview stage via a reference to the
// source stage's root layer, rather than SdfCopySpec from one local
// layer: a material pulled in by a reference (e.g. a .mtlx asset) has no
// spec in any local layer — it only exists composed — and even local
// materials can be spread across sublayers. Referencing at the identical
// path keeps internal connection paths valid without remapping, and
// relative asset paths keep resolving against their original layers.
// Authored only when the material changes: the shared in-memory layers
// recompose live, so revision bumps need no re-referencing.
if (materialPath != m_materialPath) {
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath);
if (!mirrorPrim) {
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
return;
}
mirrorPrim.GetReferences().ClearReferences();
mirrorPrim.GetReferences().AddReference(
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
}
pxr::UsdPrim shapesRoot = m_previewStage->GetPrimAtPath(kShapesRootPath);
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
if (!material) {
LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString());
return;
}
if (shapesRoot) // bound on the scope: inherits to whichever shape is active
pxr::UsdShadeMaterialBindingAPI::Apply(shapesRoot).Bind(material);
// ── Selected-node preview (Hypershade-style) ─────────────────────────
// All node-preview overrides (the scratch wrapper shader and the local
// opinions on the material's surface outputs) live exclusively in the
// preview stage's session layer, which is stronger than the root layer's
// reference. Wiping them is then just clearing that layer — no per-spec
// surgery (RemoveNameChild/RemoveProperty) that could interact badly with
// Sdf's inert-spec cleanup, and no stale overrides on other materials.
pxr::SdfLayerHandle sessionLayer = m_previewStage->GetSessionLayer();
sessionLayer->Clear();
// Route the material's surface through the selected node. Nodes outside
// the material's subtree (possible in hand-authored cross-scope networks)
// aren't composed into the scratch stage, so those keep the whole-material
// preview.
if (!previewNodePath.IsEmpty() && !previewNodeOutput.empty() &&
previewNodePath.HasPrefix(materialPath)) {
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
if (nodeShader) {
pxr::UsdEditContext sessionEdit(m_previewStage,
pxr::UsdEditTarget(sessionLayer));
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
pxr::TfToken sourceName(previewNodeOutput);
if (!previewOutputIsTerminal) {
// Pattern/texture output: show it as the diffuse color of a
// scratch UsdPreviewSurface, like Maya's swatch for textures.
pxr::UsdShadeShader wrap = pxr::UsdShadeShader::Define(
m_previewStage, materialPath.AppendChild(kNodePreviewShaderName));
wrap.CreateIdAttr(pxr::VtValue(pxr::TfToken("UsdPreviewSurface")));
wrap.CreateInput(pxr::TfToken("roughness"), pxr::SdfValueTypeNames->Float)
.Set(0.4f);
wrap.CreateInput(pxr::TfToken("diffuseColor"), pxr::SdfValueTypeNames->Color3f)
.ConnectToSource(sourceApi, sourceName);
sourceApi = wrap.ConnectableAPI();
sourceName = pxr::TfToken("surface");
}
// Override both the universal and the mtlx render-context outputs
// so the preview target wins regardless of which context the
// material authored (a .mtlx import only authors outputs:mtlx:*,
// which Storm would otherwise prefer).
material.CreateSurfaceOutput().ConnectToSource(sourceApi, sourceName);
material.CreateSurfaceOutput(pxr::TfToken("mtlx")).ConnectToSource(sourceApi, sourceName);
}
}
m_materialPath = materialPath;
m_lastGraphRevision = graphRevision;
m_previewNodePath = previewNodePath;
m_previewNodeOutput = previewNodeOutput;
m_dirty = true;
}
void MaterialPreviewRenderer::ApplyLightPreset(int index) {
if (index < 0 || index >= kLightPresetCount) index = 0;
m_lightPreset = index;
const std::string file = ResourcePath(std::string("resources/hdri/") + kLightPresets[index].file);
const bool haveHdr = std::filesystem::exists(file);
pxr::UsdPrim distant = m_previewStage->GetPrimAtPath(kDistantLightPath);
if (haveHdr) {
pxr::UsdLuxDomeLight dome = pxr::UsdLuxDomeLight::Define(m_previewStage, kDomeLightPath);
dome.CreateTextureFileAttr().Set(pxr::SdfAssetPath(file));
// Declare the projection explicitly: left at "automatic", hdArnold
// falls through to Arnold's skydome default ("angular" fisheye),
// which squeezes an equirect panorama into a blob at the center.
// Storm always samples latlong regardless, so this only affects
// spec-following delegates.
dome.CreateTextureFormatAttr().Set(pxr::UsdLuxTokens->latlong);
dome.GetPrim().SetActive(true);
if (distant) distant.SetActive(false); // dome carries the lighting alone
UpdateDomeOrientation();
} else {
LOG_WARNING("MaterialPreviewRenderer: HDRI not found (" + file +
") — falling back to distant light. Re-run CMake configure to download presets.");
if (pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath))
dome.SetActive(false);
if (distant) distant.SetActive(true);
}
m_dirty = true;
}
void MaterialPreviewRenderer::ApplyPreviewShape(int index) {
if (index < 0 || index >= kShapePresetCount) index = 0;
// Fall back to the sphere if the requested shape's asset wasn't bundled.
if (!m_previewStage->GetPrimAtPath(
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[index].primName))))
index = 0;
m_previewShape = index;
for (int i = 0; i < kShapePresetCount; ++i) {
pxr::UsdPrim prim = m_previewStage->GetPrimAtPath(
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName)));
if (prim)
prim.SetActive(i == index);
}
// Shapes differ wildly in size (the cloud VDB spans hundreds of units) —
// reframe on the active shape's bounds; inactive prims don't contribute.
m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds());
m_camera.FrameSelection(m_previewBBox, kShapePresets[index].frameFit);
m_renderer.SetCameraStateFromGfCamera(
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
m_dirty = true;
}
void MaterialPreviewRenderer::RenderShapeDropdown(float width) {
if (!m_initialized) return;
ImGui::SetNextItemWidth(width);
if (ImGui::BeginCombo("##PreviewShape", kShapePresets[m_previewShape].label)) {
for (int i = 0; i < kShapePresetCount; ++i) {
if (!m_previewStage->GetPrimAtPath(
kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName))))
continue; // asset not bundled
if (ImGui::Selectable(kShapePresets[i].label, i == m_previewShape) && i != m_previewShape)
ApplyPreviewShape(i);
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Shader-ball preview geometry");
}
void MaterialPreviewRenderer::UpdateDomeOrientation() {
if (!m_previewStage) return;
pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath);
if (!dome) return;
// Storm samples the dome texture with the pole along the light's local
// +Y (domeLight.glslfx: v = acos(dir.y)/pi), while spec-following
// delegates map it with the pole along local +Z (hdCycles feeds the
// dome-local direction to Cycles' Z-up EnvironmentTextureNode; hdArnold's
// skydome follows the same UsdLux convention). Rotating the dome -90°
// about X for those points their +Z pole at world +Y, which reproduces
// Storm's orientation exactly — including the horizontal (longitude)
// alignment, since both parameterize u from the same axes.
const pxr::TfToken rendererId = m_renderer.GetCurrentRendererId();
const bool yUpPole = rendererId.IsEmpty() ||
rendererId == pxr::TfToken("HdStormRendererPlugin");
if (rendererId == pxr::TfToken("HdArnoldRendererPlugin")) {
// With texture:format explicitly latlong, hdArnold aligns its skydome
// with Storm on its own — earlier rotation offsets here were chasing
// what turned out to be the angular-projection bug (format left
// "automatic" fell through to Arnold's fisheye default). If a real
// residual yaw shows up against Storm, put it in the Y component.
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
} else {
if(rendererId == pxr::TfToken("HdCyclesPlugin")) {
// hdCycles' latlong mapping is flipped 180° from Storm's, so add
// a yaw to match Storm's orientation. The pole is still along +Z.
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 270.0f, 0.0f));
} else {
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 0.0f, 0.0f));
}
}
m_dirty = true;
}
void MaterialPreviewRenderer::RenderLightingDropdown(float width) {
if (!m_initialized) return;
ImGui::SetNextItemWidth(width);
if (ImGui::BeginCombo("##PreviewLighting", kLightPresets[m_lightPreset].label)) {
for (int i = 0; i < kLightPresetCount; ++i) {
if (ImGui::Selectable(kLightPresets[i].label, i == m_lightPreset) && i != m_lightPreset)
ApplyLightPreset(i);
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Shader-ball HDR environment");
}
uint32_t MaterialPreviewRenderer::Render(int width, int height) {
EnsureInitialized();
if (width <= 0 || height <= 0) return 0;
// Re-render while a progressive delegate (Arnold/Cycles/Embree) is still
// accumulating — rendering only once would freeze its first noisy sample
// pass into the cached texture. Storm converges in one pass, so the
// dirty-flag caching still applies there.
if (m_dirty || width != m_lastWidth || height != m_lastHeight || !m_renderer.IsConverged()) {
// 2x supersample: the texture is displayed at (width, height) by
// ImGui, so rendering larger gives the swatch cheap anti-aliasing.
m_renderer.Render(width * 2, height * 2);
m_dirty = false;
m_lastWidth = width;
m_lastHeight = height;
}
return m_renderer.GetColorTextureID();
}
void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) {
if (!m_initialized) return;
m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity
m_renderer.SetCameraStateFromGfCamera(
m_camera.ComputeGfCamera(m_previewBBox, /*autoClip=*/true));
m_dirty = true;
}
void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string& ocioDisplay,
const std::string& ocioView,
const std::string& ocioColorSpace,
const std::string& ocioLook) {
m_renderer.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(ccMode));
m_renderer.SetOcioDisplay(ocioDisplay);
m_renderer.SetOcioView(ocioView);
m_renderer.SetOcioColorSpace(ocioColorSpace);
m_renderer.SetOcioLook(ocioLook);
m_dirty = true;
}
void MaterialPreviewRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
if (pluginId == m_renderer.GetCurrentRendererId()) return;
m_renderer.SetRendererPlugin(pluginId);
UpdateDomeOrientation(); // pole convention differs per delegate
m_dirty = true;
}
pxr::TfToken MaterialPreviewRenderer::GetRendererPlugin() const {
return m_renderer.GetCurrentRendererId();
}
void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
std::string displayName = currentId.IsEmpty()
? "Renderer"
: UsdSceneRenderer::GetRendererDisplayName(currentId);
ImGui::Button(displayName.c_str(), ImVec2(width, 0.0f));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Shader-ball render delegate");
if (ImGui::BeginPopupContextItem("MaterialPreviewRendererPopup", ImGuiPopupFlags_MouseButtonLeft)) {
for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
if (name.empty()) name = pluginId.GetString();
bool selected = (pluginId == currentId);
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
SetRendererPlugin(pluginId);
}
ImGui::EndPopup();
}
}
} // namespace UsdLayerManager
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include "../core/UsdSceneRenderer.h"
#include "../core/ViewportCamera.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/gf/bbox3d.h>
#include <cstdint>
#include <string>
namespace UsdLayerManager {
/// Shader-ball preview: mirrors the material currently open in the Material
/// Editor onto a scratch sphere and renders it through an independent Hydra
/// instance, with its own renderer-delegate switch (mirrors the viewport's
/// dropdown so the swatch can preview Storm, Cycles, etc. independently of
/// the main viewport's choice).
class MaterialPreviewRenderer {
public:
/// Mirrors the material subtree at materialPath (composed via a reference
/// to sourceStage's root layer) onto the internal scratch stage and binds
/// it to the preview sphere. graphRevision is a cheap caller-computed
/// hash of the current node/link graph + values — compared against the
/// last call so unrelated frames don't pay for a re-render (real cost
/// with a path-traced delegate).
///
/// previewNodePath (optional, Hypershade-style): the shader node selected
/// in the graph; the ball then previews that node's output instead of the
/// material's own surface. Terminal (token-typed) outputs are connected
/// as the surface directly; pattern outputs are routed into the
/// diffuseColor of a scratch UsdPreviewSurface. Pass an empty path to
/// preview the whole material.
void SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
const pxr::SdfPath& materialPath,
size_t graphRevision,
const pxr::SdfPath& previewNodePath = pxr::SdfPath(),
const std::string& previewNodeOutput = std::string(),
bool previewOutputIsTerminal = false);
/// Renders only if something changed since the last call. Returns the GL
/// color texture ID suitable for ImGui::Image().
uint32_t Render(int width, int height);
/// Click-drag orbit input, in pixel deltas (mirrors Maya's swatch orbit).
void OrbitDrag(float deltaX, float deltaY);
void SetColorCorrection(int ccMode, const std::string& ocioDisplay,
const std::string& ocioView,
const std::string& ocioColorSpace,
const std::string& ocioLook);
/// Switches the shader-ball's render delegate (what RenderRendererDropdown
/// does on click). No-op if already this plugin. Exposed so other
/// consumers of a material preview (e.g. NodeThumbnailCache's in-canvas
/// thumbnails) can mirror this renderer's current choice.
void SetRendererPlugin(const pxr::TfToken& pluginId);
/// Current render delegate (empty until EnsureInitialized() has run).
pxr::TfToken GetRendererPlugin() const;
/// True once the last Render() call's image is final. Progressive
/// delegates (Arnold/Cycles/Embree) return false for many calls after a
/// material/renderer change while they accumulate samples — callers that
/// cache the rendered texture (e.g. NodeThumbnailCache) must keep
/// re-rendering until this is true, or they'll cache an early noisy (or
/// still-black) frame.
bool IsConverged() const { return m_renderer.IsConverged(); }
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
/// width: ImGui item width (-1 = fill available).
void RenderRendererDropdown(float width = -1.0f);
/// HDR-environment picker (External / Room / Interior / Sunset). Presets
/// map to bundled resources/hdri/*.exr dome-light textures; a preset
/// whose file is missing falls back to the distant key light.
/// width: ImGui item width (-1 = fill available).
void RenderLightingDropdown(float width = -1.0f);
/// Preview-geometry picker (Sphere / Cube / Cylinder / Teapot / Hair /
/// Cloud Volume). Shapes whose asset is missing are omitted.
/// width: ImGui item width (-1 = fill available).
void RenderShapeDropdown(float width = -1.0f);
private:
void EnsureInitialized();
void ApplyLightPreset(int index);
/// Activates the chosen preview shape (deactivating the others) and
/// reframes the camera to its bounds.
void ApplyPreviewShape(int index);
/// Compensates the dome-light pole convention per render delegate: Storm
/// samples with the pole along local +Y, spec-following delegates
/// (hdArnold, hdCycles) along local +Z. Called on preset and renderer
/// switches so the HDR environment reads identically everywhere.
void UpdateDomeOrientation();
pxr::UsdStageRefPtr m_previewStage;
UsdSceneRenderer m_renderer;
ViewportCamera m_camera;
pxr::GfBBox3d m_previewBBox;
pxr::SdfPath m_materialPath; // path (identical in source & scratch stage)
size_t m_lastGraphRevision = 0;
pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material)
std::string m_previewNodeOutput;
int m_lightPreset = 0;
int m_previewShape = 0;
bool m_initialized = false;
bool m_dirty = true;
int m_lastWidth = 0;
int m_lastHeight = 0;
};
} // namespace UsdLayerManager
@@ -0,0 +1,12 @@
# pragma once
#include "imgui.h"
namespace ax {
namespace Drawing {
enum class IconType { Flow, Circle, Square, Grid, RoundSquare, Diamond };
void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor);
} // namespace Drawing
} // namespace ax
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,642 @@
//------------------------------------------------------------------------------
// Math 2D
//------------------------------------------------------------------------------
# if !defined(__AX_MATH_2D_INL__)
# define __AX_MATH_2D_INL__
# pragma once
//------------------------------------------------------------------------------
# if defined(__cplusplus)
//------------------------------------------------------------------------------
# include <utility>
# include <cmath>
//------------------------------------------------------------------------------
template <typename T, typename P>
inline ax::basic_point<T> operator + (const ax::basic_size<P>& lhs, const ax::basic_point<T>& rhs)
{
return static_cast<ax::basic_point<T>>(lhs) + rhs;
}
template <typename T, typename P>
inline ax::basic_point<T> operator + (const ax::basic_point<T>& lhs, const ax::basic_size<P>& rhs)
{
return lhs + static_cast<ax::basic_point<T>>(rhs);
}
//------------------------------------------------------------------------------
template <typename T>
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point(const point_t& p, bool on_edge, float radius) const
{
auto point = get_closest_point(p, on_edge);
const auto offset = p - point;
const auto distance_sq = offset.x * offset.x + offset.y * offset.y;
if (distance_sq <= 0)
return point;
const auto distance = sqrtf(static_cast<float>(distance_sq));
return point + static_cast<basic_point<T>>(std::min(distance, static_cast<float>(radius)) * static_cast<basic_point<float>>(offset) * (1.0f / distance));
}
template <typename T>
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point(const basic_rect& r) const
{
point_t result;
if (r.left() >= right())
result.x = right();
else if (r.right() <= left())
result.x = left();
else
result.x = (std::max(left(), r.left()) + std::min(right(), r.right())) / 2;
if (r.top() >= bottom())
result.y = bottom();
else if (r.bottom() <= top())
result.y = top();
else
result.y = (std::max(top(), r.top()) + std::min(bottom(), r.bottom())) / 2;
return result;
}
template <typename T>
inline typename ax::basic_rect<T>::point_t ax::basic_rect<T>::get_closest_point_hollow(const point_t& p, T rounding, rect_region* region) const
{
rounding = std::min(rounding, std::min(w, h) / 2);
const auto inner = expanded(-rounding);
if (p.x <= inner.left() && p.y <= inner.top())
{
if (region) *region = rect_region::top_left;
return inner.top_left().followed(p + point_t(-1, -1), rounding);
}
else if (p.x >= inner.right() && p.y <= inner.top())
{
if (region) *region = rect_region::top_right;
return inner.top_right().followed(p + point_t(1, -1), rounding);
}
else if (p.x <= inner.left() && p.y >= inner.bottom())
{
if (region) *region = rect_region::bottom_left;
return inner.bottom_left().followed(p + point_t(-1, 1), rounding);
}
else if (p.x >= inner.right() && p.y >= inner.bottom())
{
if (region) *region = rect_region::bottom_right;
return inner.bottom_right().followed(p + point_t(1, 1), rounding);
}
else
{
if (contains(p) || region)
{
const auto l_diff = abs(p.x - left());
const auto r_diff = abs(p.x - right());
const auto t_diff = abs(p.y - top());
const auto b_diff = abs(p.y - bottom());
if (l_diff <= r_diff && l_diff <= t_diff && l_diff <= b_diff)
{
if (region) *region = rect_region::left;
return point_t(left(), p.y);
}
else if (r_diff <= l_diff && r_diff <= t_diff && r_diff <= b_diff)
{
if (region) *region = rect_region::right;
return point_t(right(), p.y);
}
else if (t_diff <= l_diff && t_diff <= r_diff && t_diff <= b_diff)
{
if (region) *region = rect_region::top;
return point_t(p.x, top());
}
else
{
if (region) *region = rect_region::bottom;
return point_t(p.x, bottom());
}
}
else
return get_closest_point(p, true);
}
}
template <typename T>
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r) const
{
auto a = get_closest_point(r);
auto b = r.get_closest_point(*this);
auto distribute = [](T& a, T& b, T a0, T a1, T b0, T b1)
{
if (a0 >= b1 || a1 <= b0)
return;
const auto aw = a1 - a0;
const auto bw = b1 - b0;
if (aw > bw)
{
b = b0 + bw - bw * (a - a0) / aw;
a = b;
}
else if (aw < bw)
{
a = a0 + aw - aw * (b - b0) / bw;
b = a;
}
};
distribute(a.x, b.x, left(), right(), r.left(), r.right());
distribute(a.y, b.y, top(), bottom(), r.top(), r.bottom());
return basic_line<T>(a, b);
}
template <typename T>
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r, T radius) const
{
return get_closest_line(r, radius, radius);
}
template <typename T>
inline ax::basic_line<T> ax::basic_rect<T>::get_closest_line(const basic_rect& r, T radius_a, T radius_b) const
{
auto line = get_closest_line(r);
if (radius_a < 0)
radius_a = 0;
if (radius_b < 0)
radius_b = 0;
if (radius_a == 0 && radius_b == 0)
return line;
const auto offset = line.b - line.a;
const auto length_sq = offset.x * offset.x + offset.y * offset.y;
const auto radius_a_sq = radius_a * radius_a;
const auto radius_b_sq = radius_b * radius_b;
if (length_sq <= 0)
return line;
const auto length = sqrtf(static_cast<float>(length_sq));
const auto direction = ax::basic_point<float>(offset.x / length, offset.y / length);
const auto total_radius_sq = radius_a_sq + radius_b_sq;
float radius_a_f = static_cast<float>(radius_a);
float radius_b_f = static_cast<float>(radius_b);
if (total_radius_sq > length_sq)
{
const auto scale = length / (radius_a_f + radius_b_f);
radius_a_f *= scale;
radius_b_f *= scale;
}
line.a = line.a + static_cast<point_t>(direction * radius_a_f);
line.b = line.b - static_cast<point_t>(direction * radius_b_f);
return line;
}
//------------------------------------------------------------------------------
inline void ax::matrix::zero()
{
*this = matrix(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
}
inline void ax::matrix::reset()
{
*this = matrix();
}
inline bool ax::matrix::invert()
{
const float det = (m11 * m22 - m21 * m12);
if (det == 0.0f)
return false;
const float invDet = 1.0f / det;
*this = matrix(
m22 * invDet,
-m12 * invDet,
-m21 * invDet,
m11 * invDet,
-(-m21 * m32 + m22 * m31) * invDet,
-( m11 * m32 - m12 * m31) * invDet);
return true;
}
inline void ax::matrix::translate(float x, float y, matrix_order order/* = matrix_order::prepend*/)
{
combine(matrix(1.0f, 0.0f, 0.0f, 1.0f, x, y), order);
}
inline void ax::matrix::rotate(float angle, matrix_order order/* = matrix_order::prepend*/)
{
float angleRad = angle * AX_PI / 180.0f;
const float c = cosf(angleRad);
const float s = sinf(angleRad);
combine(matrix(c, s, -s, c, 0.0f, 0.0f), order);
}
inline void ax::matrix::rotate_at(float angle, float cx, float cy, matrix_order order/* = matrix_order::prepend*/)
{
const auto angleRad = angle * AX_PI / 180.0f;
const auto c = cosf(angleRad);
const auto s = sinf(angleRad);
combine(matrix(c, s, -s, c,
-cx * c - cy * -s + cx,
-cx * s - cy * c + cy), order);
}
inline void ax::matrix::scale(float x, float y, matrix_order order/* = matrix_order::prepend*/)
{
combine(matrix(x, 0.0f, 0.0f, y, 0.0f, 0.0f), order);
}
inline void ax::matrix::shear(float x, float y, matrix_order order/* = matrix_order::prepend*/)
{
combine(matrix(1.0f, y, x, 1.0f, 0.0f, 0.0f), order);
}
inline void ax::matrix::combine(const matrix& matrix, matrix_order order/* = matrix_order::prepend*/)
{
if (order == matrix_order::set)
{
if (this != &matrix)
*this = matrix;
return;
}
const auto* am = this;
const auto* bm = &matrix;
if (order == matrix_order::append)
{
using std::swap;
swap(am, bm);
}
*this = ax::matrix(
am->m11 * bm->m11 + am->m21 * bm->m12,
bm->m11 * am->m12 + bm->m12 * am->m22,
bm->m21 * am->m11 + bm->m22 * am->m21,
am->m12 * bm->m21 + am->m22 * bm->m22,
bm->m31 * am->m11 + bm->m32 * am->m21 + am->m31,
bm->m31 * am->m12 + bm->m32 * am->m22 + am->m32);
}
inline ax::matrix ax::matrix::inverted() const
{
matrix inverted = *this;
inverted.invert();
return inverted;
}
//------------------------------------------------------------------------------
inline void ax::matrix4::zero()
{
*this = matrix4(
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f);
}
inline void ax::matrix4::reset()
{
*this = matrix4();
}
inline bool ax::matrix4::invert()
{
# if 0
// wild magic 4 inverse - http://www.geometrictools.com/Documentation/LaplaceExpansionTheorem.pdf
// 84 multiplications
// 66 adds/subs
// 1 division
// 24
const float fA0 = m11 * m22 - m12 * m21;
const float fA1 = m11 * m23 - m13 * m21;
const float fA2 = m11 * m24 - m14 * m21;
const float fA3 = m12 * m23 - m13 * m22;
const float fA4 = m12 * m24 - m14 * m22;
const float fA5 = m13 * m24 - m14 * m23;
const float fB0 = m31 * m42 - m32 * m41;
const float fB1 = m31 * m43 - m33 * m41;
const float fB2 = m31 * m44 - m34 * m41;
const float fB3 = m32 * m43 - m33 * m42;
const float fB4 = m32 * m44 - m34 * m42;
const float fB5 = m33 * m44 - m34 * m43;
// 6
const float det = fA0 * fB5 - fA1 * fB4 + fA2 * fB3 + fA3 * fB2 - fA4 * fB1 + fA5 * fB0;
if (det == 0.0f)
{
zero();
return false;
}
const float invDet = 1.0f / det;
// 36 + 16
*this = Matrix4(
( m22 * fB5 - m23 * fB4 + m24 * fB3) * invDet,
(-m12 * fB5 + m13 * fB4 - m14 * fB3) * invDet,
( m42 * fA5 - m43 * fA4 + m44 * fA3) * invDet,
(-m32 * fA5 + m33 * fA4 - m34 * fA3) * invDet,
(-m21 * fB5 + m23 * fB2 - m24 * fB1) * invDet,
( m11 * fB5 - m13 * fB2 + m14 * fB1) * invDet,
(-m41 * fA5 + m43 * fA2 - m44 * fA1) * invDet,
( m31 * fA5 - m33 * fA2 + m34 * fA1) * invDet,
( m21 * fB4 - m22 * fB2 + m24 * fB0) * invDet,
(-m11 * fB4 + m12 * fB2 - m14 * fB0) * invDet,
( m41 * fA4 - m42 * fA2 + m44 * fA0) * invDet,
(-m31 * fA4 + m32 * fA2 - m34 * fA0) * invDet,
(-m21 * fB3 + m22 * fB1 - m23 * fB0) * invDet,
( m11 * fB3 - m12 * fB1 + m13 * fB0) * invDet,
(-m41 * fA3 + m42 * fA1 - m43 * fA0) * invDet,
( m31 * fA3 - m32 * fA1 + m33 * fA0) * invDet);
return true;
# else
// 214 multiplications
// 80 adds/subs
// 1 division
const float d00 = m22 * m33 * m44 + m32 * m43 * m24 + m42 * m23 * m34 - m24 * m33 * m42 - m34 * m43 * m22 - m44 * m23 * m32;
const float d01 = m12 * m33 * m44 + m32 * m43 * m14 + m42 * m13 * m34 - m14 * m33 * m42 - m34 * m43 * m12 - m44 * m13 * m32;
const float d02 = m12 * m23 * m44 + m22 * m43 * m14 + m42 * m13 * m24 - m14 * m23 * m42 - m24 * m43 * m12 - m44 * m13 * m22;
const float d03 = m12 * m23 * m34 + m22 * m33 * m14 + m32 * m13 * m24 - m14 * m23 * m32 - m24 * m33 * m12 - m34 * m13 * m22;
const float d10 = m21 * m33 * m44 + m31 * m43 * m24 + m41 * m23 * m34 - m24 * m33 * m41 - m34 * m43 * m21 - m44 * m23 * m31;
const float d11 = m11 * m33 * m44 + m31 * m43 * m14 + m41 * m13 * m34 - m14 * m33 * m41 - m34 * m43 * m11 - m44 * m13 * m31;
const float d12 = m11 * m23 * m44 + m21 * m43 * m14 + m41 * m13 * m24 - m14 * m23 * m41 - m24 * m43 * m11 - m44 * m13 * m21;
const float d13 = m11 * m23 * m34 + m21 * m33 * m14 + m31 * m13 * m24 - m14 * m23 * m31 - m24 * m33 * m11 - m34 * m13 * m21;
const float d20 = m21 * m32 * m44 + m31 * m42 * m24 + m41 * m22 * m34 - m24 * m32 * m41 - m34 * m42 * m21 - m44 * m22 * m31;
const float d21 = m11 * m32 * m44 + m31 * m42 * m14 + m41 * m12 * m34 - m14 * m32 * m41 - m34 * m42 * m11 - m44 * m12 * m31;
const float d22 = m11 * m22 * m44 + m21 * m42 * m14 + m41 * m12 * m24 - m14 * m22 * m41 - m24 * m42 * m11 - m44 * m12 * m21;
const float d23 = m11 * m22 * m34 + m21 * m32 * m14 + m31 * m12 * m24 - m14 * m22 * m31 - m24 * m32 * m11 - m34 * m12 * m21;
const float d30 = m21 * m32 * m43 + m31 * m42 * m23 + m41 * m22 * m33 - m23 * m32 * m41 - m33 * m42 * m21 - m43 * m22 * m31;
const float d31 = m11 * m32 * m43 + m31 * m42 * m13 + m41 * m12 * m33 - m13 * m32 * m41 - m33 * m42 * m11 - m43 * m12 * m31;
const float d32 = m11 * m22 * m43 + m21 * m42 * m13 + m41 * m12 * m23 - m13 * m22 * m41 - m23 * m42 * m11 - m43 * m12 * m21;
const float d33 = m11 * m22 * m33 + m21 * m32 * m13 + m31 * m12 * m23 - m13 * m22 * m31 - m23 * m32 * m11 - m33 * m12 * m21;
const float det = m11 * d00 - m21 * d01 + m31 * d02 - m41 * d03;
if (det == 0.0f)
{
zero();
return false;
}
const float invDet = 1.0f / det;
m11 = d00 * invDet; m21 = -d10 * invDet; m31 = d20 * invDet; m41 = -d30 * invDet;
m12 = -d01 * invDet; m22 = d11 * invDet; m32 = -d21 * invDet; m42 = d31 * invDet;
m13 = d02 * invDet; m23 = -d12 * invDet; m33 = d22 * invDet; m43 = -d32 * invDet;
m14 = -d03 * invDet; m24 = d13 * invDet; m34 = -d23 * invDet; m44 = d33 * invDet;
return true;
# endif
}
inline void ax::matrix4::transpose()
{
using std::swap;
swap(m12, m21);
swap(m13, m31);
swap(m14, m41);
swap(m23, m32);
swap(m24, m42);
swap(m34, m43);
}
inline void ax::matrix4::translate(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
{
combine(matrix4(
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
x, y, z, 1.0f), order);
}
inline void ax::matrix4::rotate_x(float angle, matrix_order order/* = matrix_order::prepend*/)
{
const auto angleRad = angle * AX_PI / 180.0f;
const auto c = cosf(angleRad);
const auto s = sinf(angleRad);
combine(matrix4(
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, c, -s, 0.0f,
0.0f, s, c, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f), order);
}
inline void ax::matrix4::rotate_y(float angle, matrix_order order/* = matrix_order::prepend*/)
{
const auto angleRad = angle * AX_PI / 180.0f;
const auto c = cosf(angleRad);
const auto s = sinf(angleRad);
combine(matrix4(
c, 0.0f, -s, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
s, 0.0f, c, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f), order);
}
inline void ax::matrix4::rotate_z(float angle, matrix_order order/* = matrix_order::prepend*/)
{
const auto angleRad = angle * AX_PI / 180.0f;
const auto c = cosf(angleRad);
const auto s = sinf(angleRad);
combine(matrix4(
c, -s, 0.0f, 0.0f,
s, c, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f), order);
}
inline void ax::matrix4::rotate_axis(float angle, float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
{
const auto angleRad = angle * AX_PI / 180.0f;
const auto c = cosf(angleRad);
const auto s = sinf(angleRad);
const auto rc = 1.0f - c;
combine(matrix4(
(rc * x * x) + c, (rc * x * y) + (z * s), (rc * x * z) - (y * s), 0.0f,
(rc * x * y) - (z * s), (rc * y * y) + c, (rc * z * y) + (x * s), 0.0f,
(rc * x * z) + (y * s), (rc * y * z) - (x * s), (rc * z * z) + c, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f), order);
}
inline void ax::matrix4::scale(float x, float y, float z, matrix_order order/* = matrix_order::prepend*/)
{
combine(matrix4(
x, 0.0f, 0.0f, 0.0f,
0.0f, y, 0.0f, 0.0f,
0.0f, 0.0f, z, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f), order);
}
inline void ax::matrix4::combine(const matrix4& matrix, matrix_order order/* = matrix_order::prepend*/)
{
if (order == matrix_order::set)
{
if (this != &matrix)
*this = matrix;
return;
}
const auto* am = this;
const auto* bm = &matrix;
if (order == matrix_order::append)
{
using std::swap;
swap(am, bm);
}
*this = matrix4(
am->m11 * bm->m11 + am->m21 * bm->m12 + am->m31 * bm->m13 + am->m41 * bm->m14,
am->m12 * bm->m11 + am->m22 * bm->m12 + am->m32 * bm->m13 + am->m42 * bm->m14,
am->m13 * bm->m11 + am->m23 * bm->m12 + am->m33 * bm->m13 + am->m43 * bm->m14,
am->m14 * bm->m11 + am->m24 * bm->m12 + am->m34 * bm->m13 + am->m44 * bm->m14,
am->m11 * bm->m21 + am->m21 * bm->m22 + am->m31 * bm->m23 + am->m41 * bm->m24,
am->m12 * bm->m21 + am->m22 * bm->m22 + am->m32 * bm->m23 + am->m42 * bm->m24,
am->m13 * bm->m21 + am->m23 * bm->m22 + am->m33 * bm->m23 + am->m43 * bm->m24,
am->m14 * bm->m21 + am->m24 * bm->m22 + am->m34 * bm->m23 + am->m44 * bm->m24,
am->m11 * bm->m31 + am->m21 * bm->m32 + am->m31 * bm->m33 + am->m41 * bm->m34,
am->m12 * bm->m31 + am->m22 * bm->m32 + am->m32 * bm->m33 + am->m42 * bm->m34,
am->m13 * bm->m31 + am->m23 * bm->m32 + am->m33 * bm->m33 + am->m43 * bm->m34,
am->m14 * bm->m31 + am->m24 * bm->m32 + am->m34 * bm->m33 + am->m44 * bm->m34,
am->m11 * bm->m41 + am->m21 * bm->m42 + am->m31 * bm->m43 + am->m41 * bm->m44,
am->m12 * bm->m41 + am->m22 * bm->m42 + am->m32 * bm->m43 + am->m42 * bm->m44,
am->m13 * bm->m41 + am->m23 * bm->m42 + am->m33 * bm->m43 + am->m43 * bm->m44,
am->m14 * bm->m41 + am->m24 * bm->m42 + am->m34 * bm->m43 + am->m44 * bm->m44);
}
inline ax::matrix4 ax::matrix4::inverted() const
{
matrix4 inverted = *this;
inverted.invert();
return inverted;
}
inline ax::matrix4 ax::matrix4::transposed() const
{
matrix4 transposed = *this;
transposed.transpose();
return transposed;
}
//------------------------------------------------------------------------------
namespace ax {
namespace detail {
# if defined(__clang__) || defined(__GNUC__)
template <typename M, typename T>
void transform_points(const M& m, basic_point<T>* points, size_t count);
# else
template <typename M, typename T>
inline void transform_points(const M& m, basic_point<T>* points, size_t count)
{
// UsdLayerManager local patch: static_assert(false, ...) here is
// ill-formed regardless of instantiation (its condition doesn't depend
// on a template parameter) — stricter MSVC (/permissive-, set for MSVC
// >= 1910 in this project's CMakeLists.txt) diagnoses it just from this
// primary template's declaration, even when every real call resolves to
// one of the matrix/matrix4 specializations below and this fallback is
// never instantiated. Make the condition depend on T so it only fires
// if something actually instantiates this unspecialized fallback.
static_assert(sizeof(T) == 0, "This combination of matrix type and point type is not supported");
}
# endif
template <typename M, typename T>
inline void transform_vectors(const M& m, basic_point<T>* points, size_t count)
{
typedef basic_point<T> point_t;
for (size_t i = 0; i < count; ++i, ++points)
{
auto x = m.m11 * points->x + m.m21 * points->y;
auto y = m.m12 * points->x + m.m22 * points->y;
points->x = static_cast<typename point_t::value_type>(x);
points->y = static_cast<typename point_t::value_type>(y);
}
}
template <typename T>
inline void transform_points(const matrix& m, basic_point<T>* points, size_t count)
{
typedef basic_point<T> point_t;
for (size_t i = 0; i < count; ++i, ++points)
{
auto x = m.m11 * points->x + m.m21 * points->y + m.m31;
auto y = m.m12 * points->x + m.m22 * points->y + m.m32;
points->x = static_cast<typename point_t::value_type>(x);
points->y = static_cast<typename point_t::value_type>(y);
}
}
template <typename T>
inline void transform_points(const matrix4& m, basic_point<T>* points, size_t count)
{
typedef basic_point<T> point_t;
for (size_t i = 0; i < count; ++i, ++points)
{
auto x = m.m11 * points->x + m.m21 * points->y + m.m41;
auto y = m.m12 * points->x + m.m22 * points->y + m.m42;
points->x = static_cast<typename point_t::value_type>(x);
points->y = static_cast<typename point_t::value_type>(y);
}
}
} // namespace detail
} // namespace ax
//------------------------------------------------------------------------------
# endif // defined(__cplusplus)
//------------------------------------------------------------------------------
# endif // __AX_MATH_2D_INL__
@@ -0,0 +1,13 @@
#pragma once
#include "imgui.h"
#include "Drawing.h"
namespace ax {
namespace Widgets {
using Drawing::IconType;
void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0));
} // namespace Widgets
} // namespace ax
@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# pragma once
# include "imgui.h"
# include "ax/Math2D.h"
//------------------------------------------------------------------------------
//namespace ax {
//namespace ImGuiInterop {
//------------------------------------------------------------------------------
// Modern ImGui (1.89.4+) already defines these operators in imgui.h itself
// when IMGUI_DEFINE_MATH_OPERATORS is set before its first include — guard
// against redefinition against this project's ImGui version.
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; }
static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; }
static inline ImVec2 operator+(const ImVec2& lhs) { return ImVec2( lhs.x, lhs.y); }
static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); }
static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y); }
static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y); }
static inline ImVec2 operator*(const ImVec2& lhs, float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); }
static inline ImVec2 operator*(float lhs, const ImVec2& rhs) { return ImVec2(lhs * rhs.x, lhs * rhs.y); }
# endif
static inline int roundi(float value) { return static_cast<int>(value); }
static inline ax::point to_point(const ImVec2& value) { return ax::point(roundi(value.x), roundi(value.y)); }
static inline ax::pointf to_pointf(const ImVec2& value) { return ax::pointf(value.x, value.y); }
static inline ax::pointf to_pointf(const ax::point& value) { return ax::pointf(static_cast<float>(value.x), static_cast<float>(value.y)); }
static inline ax::size to_size (const ImVec2& value) { return ax::size (roundi(value.x), roundi(value.y)); }
static inline ax::sizef to_sizef(const ImVec2& value) { return ax::sizef(value.x, value.y); }
static inline ImVec2 to_imvec(const ax::point& value) { return ImVec2(static_cast<float>(value.x), static_cast<float>(value.y)); }
static inline ImVec2 to_imvec(const ax::pointf& value) { return ImVec2(value.x, value.y); }
static inline ImVec2 to_imvec(const ax::size& value) { return ImVec2(static_cast<float>(value.w), static_cast<float>(value.h)); }
static inline ImVec2 to_imvec(const ax::sizef& value) { return ImVec2(value.w, value.h); }
static inline ax::rect ImGui_GetItemRect() { return ax::rect(to_point(ImGui::GetItemRectMin()), to_point(ImGui::GetItemRectMax())); }
//------------------------------------------------------------------------------
struct FringeScaleScope
{
FringeScaleScope(float scale)
: m_LastFringeScale(ImGui::GetWindowDrawList()->_FringeScale)
{
ImGui::GetWindowDrawList()->_FringeScale = scale;
}
~FringeScaleScope()
{
ImGui::GetWindowDrawList()->_FringeScale = m_LastFringeScale;
}
private:
float m_LastFringeScale;
};
//------------------------------------------------------------------------------
//} // namespace ImGuiInterop
//} // namespace ax
@@ -0,0 +1,207 @@
//# include "stdafx.h"
# include "ax/Drawing.h"
# include "ax/Math2D.h"
# include "Interop.h"
# include <cmath>
void ax::Drawing::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor)
{
auto rect = ax::rect(to_point(a), to_point(b));
const auto outline_scale = rect.w / 24.0f;
const auto extra_segments = roundi(2 * outline_scale); // for full circle
if (type == IconType::Flow)
{
const auto origin_scale = rect.w / 24.0f;
const auto offset_x = 1.0f * origin_scale;
const auto offset_y = 0.0f * origin_scale;
const auto margin = (filled ? 2.0f : 2.0f) * origin_scale;
const auto rounding = 0.1f * origin_scale;
const auto tip_round = 0.7f; // percentage of triangle edge (for tip)
//const auto edge_round = 0.7f; // percentage of triangle edge (for corner)
const auto canvas = rectf(
rect.x + margin + offset_x,
rect.y + margin + offset_y,
rect.w - margin * 2.0f,
rect.h - margin * 2.0f);
const auto left = canvas.x + canvas.w * 0.5f * 0.3f;
const auto right = canvas.x + canvas.w - canvas.w * 0.5f * 0.3f;
const auto top = canvas.y + canvas.h * 0.5f * 0.2f;
const auto bottom = canvas.y + canvas.h - canvas.h * 0.5f * 0.2f;
const auto center_y = (top + bottom) * 0.5f;
//const auto angle = AX_PI * 0.5f * 0.5f * 0.5f;
const auto tip_top = ImVec2(canvas.x + canvas.w * 0.5f, top);
const auto tip_right = ImVec2(right, center_y);
const auto tip_bottom = ImVec2(canvas.x + canvas.w * 0.5f, bottom);
drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding));
drawList->PathBezierCubicCurveTo(
ImVec2(left, top),
ImVec2(left, top),
ImVec2(left, top) + ImVec2(rounding, 0));
drawList->PathLineTo(tip_top);
drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round);
drawList->PathBezierCubicCurveTo(
tip_right,
tip_right,
tip_bottom + (tip_right - tip_bottom) * tip_round);
drawList->PathLineTo(tip_bottom);
drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0));
drawList->PathBezierCubicCurveTo(
ImVec2(left, bottom),
ImVec2(left, bottom),
ImVec2(left, bottom) - ImVec2(0, rounding));
if (!filled)
{
if (innerColor & 0xFF000000)
drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
drawList->PathStroke(color, true, 2.0f * outline_scale);
}
else
drawList->PathFillConvex(color);
}
else
{
auto triangleStart = rect.center_x() + 0.32f * rect.w;
rect.x -= roundi(rect.w * 0.25f * 0.25f);
if (type == IconType::Circle)
{
const auto c = to_imvec(rect.center());
if (!filled)
{
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
if (innerColor & 0xFF000000)
drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments);
drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale);
}
else
drawList->AddCircleFilled(c, 0.5f * rect.w / 2.0f, color, 12 + extra_segments);
}
if (type == IconType::Square)
{
if (filled)
{
const auto r = 0.5f * rect.w / 2.0f;
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments);
}
else
{
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
if (innerColor & 0xFF000000)
drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments);
drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale);
}
}
if (type == IconType::Grid)
{
const auto r = 0.5f * rect.w / 2.0f;
const auto w = ceilf(r / 3.0f);
const auto baseTl = ImVec2(floorf(rect.center_x() - w * 2.5f), floorf(rect.center_y() - w * 2.5f));
const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w));
auto tl = baseTl;
auto br = baseBr;
for (int i = 0; i < 3; ++i)
{
tl.x = baseTl.x;
br.x = baseBr.x;
drawList->AddRectFilled(tl, br, color);
tl.x += w * 2;
br.x += w * 2;
if (i != 1 || filled)
drawList->AddRectFilled(tl, br, color);
tl.x += w * 2;
br.x += w * 2;
drawList->AddRectFilled(tl, br, color);
tl.y += w * 2;
br.y += w * 2;
}
triangleStart = br.x + w + 1.0f / 24.0f * rect.w;
}
if (type == IconType::RoundSquare)
{
if (filled)
{
const auto r = 0.5f * rect.w / 2.0f;
const auto cr = r * 0.5f;
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
drawList->AddRectFilled(p0, p1, color, cr, 15);
}
else
{
const auto r = 0.5f * rect.w / 2.0f - 0.5f;
const auto cr = r * 0.5f;
const auto p0 = to_imvec(rect.center()) - ImVec2(r, r);
const auto p1 = to_imvec(rect.center()) + ImVec2(r, r);
if (innerColor & 0xFF000000)
drawList->AddRectFilled(p0, p1, innerColor, cr, 15);
drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale);
}
}
else if (type == IconType::Diamond)
{
if (filled)
{
const auto r = 0.607f * rect.w / 2.0f;
const auto c = rect.center();
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r));
drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0));
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r));
drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0));
drawList->PathFillConvex(color);
}
else
{
const auto r = 0.607f * rect.w / 2.0f - 0.5f;
const auto c = rect.center();
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, -r));
drawList->PathLineTo(to_imvec(c) + ImVec2( r, 0));
drawList->PathLineTo(to_imvec(c) + ImVec2( 0, r));
drawList->PathLineTo(to_imvec(c) + ImVec2(-r, 0));
if (innerColor & 0xFF000000)
drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor);
drawList->PathStroke(color, true, 2.0f * outline_scale);
}
}
else
{
const auto triangleTip = triangleStart + rect.w * (0.45f - 0.32f);
drawList->AddTriangleFilled(
ImVec2(ceilf(triangleTip), rect.top() + rect.h * 0.5f),
ImVec2(triangleStart, rect.center_y() + 0.15f * rect.h),
ImVec2(triangleStart, rect.center_y() - 0.15f * rect.h),
color);
}
}
}
@@ -0,0 +1,15 @@
#include "ax/Widgets.h"
#include "Interop.h"
void ax::Widgets::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, const ImVec4& innerColor/* = ImVec4(0, 0, 0, 0)*/)
{
if (ImGui::IsRectVisible(size))
{
auto cursorPos = ImGui::GetCursorScreenPos();
auto drawList = ImGui::GetWindowDrawList();
ax::Drawing::DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor));
}
ImGui::Dummy(size);
}
@@ -0,0 +1,442 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_NODE_EDITOR_H__
# define __IMGUI_NODE_EDITOR_H__
# pragma once
//------------------------------------------------------------------------------
# include <imgui.h>
# include <cstdint> // std::uintXX_t
# include <utility> // std::move
//------------------------------------------------------------------------------
namespace ax {
namespace NodeEditor {
//------------------------------------------------------------------------------
struct NodeId;
struct LinkId;
struct PinId;
//------------------------------------------------------------------------------
enum class SaveReasonFlags: uint32_t
{
None = 0x00000000,
Navigation = 0x00000001,
Position = 0x00000002,
Size = 0x00000004,
Selection = 0x00000008,
User = 0x00000010
};
inline SaveReasonFlags operator |(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)); }
inline SaveReasonFlags operator &(SaveReasonFlags lhs, SaveReasonFlags rhs) { return static_cast<SaveReasonFlags>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)); }
using ConfigSaveSettings = bool (*)(const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
using ConfigLoadSettings = size_t (*)(char* data, void* userPointer);
using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, size_t size, SaveReasonFlags reason, void* userPointer);
using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, void* userPointer);
using ConfigSession = void (*)(void* userPointer);
struct Config
{
const char* SettingsFile;
ConfigSession BeginSaveSession;
ConfigSession EndSaveSession;
ConfigSaveSettings SaveSettings;
ConfigLoadSettings LoadSettings;
ConfigSaveNodeSettings SaveNodeSettings;
ConfigLoadNodeSettings LoadNodeSettings;
void* UserPointer;
Config()
: SettingsFile("NodeEditor.json")
, BeginSaveSession(nullptr)
, EndSaveSession(nullptr)
, SaveSettings(nullptr)
, LoadSettings(nullptr)
, SaveNodeSettings(nullptr)
, LoadNodeSettings(nullptr)
, UserPointer(nullptr)
{
}
};
//------------------------------------------------------------------------------
enum class PinKind
{
Input,
Output
};
//------------------------------------------------------------------------------
enum StyleColor
{
StyleColor_Bg,
StyleColor_Grid,
StyleColor_NodeBg,
StyleColor_NodeBorder,
StyleColor_HovNodeBorder,
StyleColor_SelNodeBorder,
StyleColor_NodeSelRect,
StyleColor_NodeSelRectBorder,
StyleColor_HovLinkBorder,
StyleColor_SelLinkBorder,
StyleColor_LinkSelRect,
StyleColor_LinkSelRectBorder,
StyleColor_PinRect,
StyleColor_PinRectBorder,
StyleColor_Flow,
StyleColor_FlowMarker,
StyleColor_GroupBg,
StyleColor_GroupBorder,
StyleColor_Count
};
enum StyleVar
{
StyleVar_NodePadding,
StyleVar_NodeRounding,
StyleVar_NodeBorderWidth,
StyleVar_HoveredNodeBorderWidth,
StyleVar_SelectedNodeBorderWidth,
StyleVar_PinRounding,
StyleVar_PinBorderWidth,
StyleVar_LinkStrength,
StyleVar_SourceDirection,
StyleVar_TargetDirection,
StyleVar_ScrollDuration,
StyleVar_FlowMarkerDistance,
StyleVar_FlowSpeed,
StyleVar_FlowDuration,
StyleVar_PivotAlignment,
StyleVar_PivotSize,
StyleVar_PivotScale,
StyleVar_PinCorners,
StyleVar_PinRadius,
StyleVar_PinArrowSize,
StyleVar_PinArrowWidth,
StyleVar_GroupRounding,
StyleVar_GroupBorderWidth,
};
struct Style
{
ImVec4 NodePadding;
float NodeRounding;
float NodeBorderWidth;
float HoveredNodeBorderWidth;
float SelectedNodeBorderWidth;
float PinRounding;
float PinBorderWidth;
float LinkStrength;
ImVec2 SourceDirection;
ImVec2 TargetDirection;
float ScrollDuration;
float FlowMarkerDistance;
float FlowSpeed;
float FlowDuration;
ImVec2 PivotAlignment;
ImVec2 PivotSize;
ImVec2 PivotScale;
float PinCorners;
float PinRadius;
float PinArrowSize;
float PinArrowWidth;
float GroupRounding;
float GroupBorderWidth;
ImVec4 Colors[StyleColor_Count];
Style()
{
NodePadding = ImVec4(8, 8, 8, 8);
NodeRounding = 12.0f;
NodeBorderWidth = 1.5f;
HoveredNodeBorderWidth = 3.5f;
SelectedNodeBorderWidth = 3.5f;
PinRounding = 4.0f;
PinBorderWidth = 0.0f;
LinkStrength = 100.0f;
SourceDirection = ImVec2(1.0f, 0.0f);
TargetDirection = ImVec2(-1.0f, 0.0f);
ScrollDuration = 0.35f;
FlowMarkerDistance = 30.0f;
FlowSpeed = 150.0f;
FlowDuration = 2.0f;
PivotAlignment = ImVec2(0.5f, 0.5f);
PivotSize = ImVec2(-1, -1);
PivotScale = ImVec2(1, 1);
PinCorners = ImDrawFlags_RoundCornersAll; // ImDrawCornerFlags_All renamed upstream in ImGui
PinRadius = 0.0f;
PinArrowSize = 0.0f;
PinArrowWidth = 0.0f;
GroupRounding = 6.0f;
GroupBorderWidth = 1.0f;
Colors[StyleColor_Bg] = ImColor( 60, 60, 70, 200);
Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40);
Colors[StyleColor_NodeBg] = ImColor( 32, 32, 32, 200);
Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96);
Colors[StyleColor_HovNodeBorder] = ImColor( 50, 176, 255, 255);
Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255);
Colors[StyleColor_NodeSelRect] = ImColor( 5, 130, 255, 64);
Colors[StyleColor_NodeSelRectBorder] = ImColor( 5, 130, 255, 128);
Colors[StyleColor_HovLinkBorder] = ImColor( 50, 176, 255, 255);
Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255);
Colors[StyleColor_LinkSelRect] = ImColor( 5, 130, 255, 64);
Colors[StyleColor_LinkSelRectBorder] = ImColor( 5, 130, 255, 128);
Colors[StyleColor_PinRect] = ImColor( 60, 180, 255, 100);
Colors[StyleColor_PinRectBorder] = ImColor( 60, 180, 255, 128);
Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255);
Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255);
Colors[StyleColor_GroupBg] = ImColor( 0, 0, 0, 160);
Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32);
}
};
//------------------------------------------------------------------------------
struct EditorContext;
//------------------------------------------------------------------------------
void SetCurrentEditor(EditorContext* ctx);
EditorContext* GetCurrentEditor();
EditorContext* CreateEditor(const Config* config = nullptr);
void DestroyEditor(EditorContext* ctx);
Style& GetStyle();
const char* GetStyleColorName(StyleColor colorIndex);
void PushStyleColor(StyleColor colorIndex, const ImVec4& color);
void PopStyleColor(int count = 1);
void PushStyleVar(StyleVar varIndex, float value);
void PushStyleVar(StyleVar varIndex, const ImVec2& value);
void PushStyleVar(StyleVar varIndex, const ImVec4& value);
void PopStyleVar(int count = 1);
void Begin(const char* id, const ImVec2& size = ImVec2(0, 0));
void End();
void BeginNode(NodeId id);
void BeginPin(PinId id, PinKind kind);
void PinRect(const ImVec2& a, const ImVec2& b);
void PinPivotRect(const ImVec2& a, const ImVec2& b);
void PinPivotSize(const ImVec2& size);
void PinPivotScale(const ImVec2& scale);
void PinPivotAlignment(const ImVec2& alignment);
void EndPin();
void Group(const ImVec2& size);
void EndNode();
bool BeginGroupHint(NodeId nodeId);
ImVec2 GetGroupMin();
ImVec2 GetGroupMax();
ImDrawList* GetHintForegroundDrawList();
ImDrawList* GetHintBackgroundDrawList();
void EndGroupHint();
// TODO: Add a way to manage node background channels
ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId);
bool Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
void Flow(LinkId linkId);
bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), float thickness = 1.0f);
bool QueryNewLink(PinId* startId, PinId* endId);
bool QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness = 1.0f);
bool QueryNewNode(PinId* pinId);
bool QueryNewNode(PinId* pinId, const ImVec4& color, float thickness = 1.0f);
bool AcceptNewItem();
bool AcceptNewItem(const ImVec4& color, float thickness = 1.0f);
void RejectNewItem();
void RejectNewItem(const ImVec4& color, float thickness = 1.0f);
void EndCreate();
bool BeginDelete();
bool QueryDeletedLink(LinkId* linkId, PinId* startId = nullptr, PinId* endId = nullptr);
bool QueryDeletedNode(NodeId* nodeId);
bool AcceptDeletedItem();
void RejectDeletedItem();
void EndDelete();
void SetNodePosition(NodeId nodeId, const ImVec2& editorPosition);
ImVec2 GetNodePosition(NodeId nodeId);
ImVec2 GetNodeSize(NodeId nodeId);
void CenterNodeOnScreen(NodeId nodeId);
void RestoreNodeState(NodeId nodeId);
void Suspend();
void Resume();
bool IsSuspended();
bool IsActive();
bool HasSelectionChanged();
int GetSelectedObjectCount();
int GetSelectedNodes(NodeId* nodes, int size);
int GetSelectedLinks(LinkId* links, int size);
void ClearSelection();
void SelectNode(NodeId nodeId, bool append = false);
void SelectLink(LinkId linkId, bool append = false);
void DeselectNode(NodeId nodeId);
void DeselectLink(LinkId linkId);
bool DeleteNode(NodeId nodeId);
bool DeleteLink(LinkId linkId);
void NavigateToContent(float duration = -1);
void NavigateToSelection(bool zoomIn = false, float duration = -1);
bool ShowNodeContextMenu(NodeId* nodeId);
bool ShowPinContextMenu(PinId* pinId);
bool ShowLinkContextMenu(LinkId* linkId);
bool ShowBackgroundContextMenu();
void EnableShortcuts(bool enable);
bool AreShortcutsEnabled();
bool BeginShortcut();
bool AcceptCut();
bool AcceptCopy();
bool AcceptPaste();
bool AcceptDuplicate();
bool AcceptCreateNode();
int GetActionContextSize();
int GetActionContextNodes(NodeId* nodes, int size);
int GetActionContextLinks(LinkId* links, int size);
void EndShortcut();
float GetCurrentZoom();
NodeId GetDoubleClickedNode();
PinId GetDoubleClickedPin();
LinkId GetDoubleClickedLink();
bool IsBackgroundClicked();
bool IsBackgroundDoubleClicked();
bool PinHadAnyLinks(PinId pinId);
ImVec2 GetScreenSize();
ImVec2 ScreenToCanvas(const ImVec2& pos);
ImVec2 CanvasToScreen(const ImVec2& pos);
//------------------------------------------------------------------------------
namespace Details {
template <typename T, typename Tag>
struct SafeType
{
SafeType(T t)
: m_Value(std::move(t))
{
}
SafeType(const SafeType&) = default;
template <typename T2, typename Tag2>
SafeType(
const SafeType
<
typename std::enable_if<!std::is_same<T, T2>::value, T2>::type,
typename std::enable_if<!std::is_same<Tag, Tag2>::value, Tag2>::type
>&) = delete;
SafeType& operator=(const SafeType&) = default;
explicit operator T() const { return Get(); }
T Get() const { return m_Value; }
private:
T m_Value;
};
template <typename Tag>
struct SafePointerType
: SafeType<uintptr_t, Tag>
{
static const Tag Invalid;
using SafeType<uintptr_t, Tag>::SafeType;
SafePointerType()
: SafePointerType(Invalid)
{
}
template <typename T = void> explicit SafePointerType(T* ptr): SafePointerType(reinterpret_cast<uintptr_t>(ptr)) {}
template <typename T = void> T* AsPointer() const { return reinterpret_cast<T*>(this->Get()); }
explicit operator bool() const { return *this != Invalid; }
};
template <typename Tag>
const Tag SafePointerType<Tag>::Invalid = { 0 };
template <typename Tag>
inline bool operator==(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
{
return lhs.Get() == rhs.Get();
}
template <typename Tag>
inline bool operator!=(const SafePointerType<Tag>& lhs, const SafePointerType<Tag>& rhs)
{
return lhs.Get() != rhs.Get();
}
} // namespace Details
struct NodeId final: Details::SafePointerType<NodeId>
{
using SafePointerType::SafePointerType;
};
struct LinkId final: Details::SafePointerType<LinkId>
{
using SafePointerType::SafePointerType;
};
struct PinId final: Details::SafePointerType<PinId>
{
using SafePointerType::SafePointerType;
};
//------------------------------------------------------------------------------
} // namespace Editor
} // namespace ax
//------------------------------------------------------------------------------
# endif // __IMGUI_NODE_EDITOR_H__
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Michał Cichoń
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+814
View File
@@ -0,0 +1,814 @@
// Crude implementation of JSON value object and parser.
//
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
# include "crude_json.h"
# include <iomanip>
# include <limits>
# include <cstdlib>
# include <clocale>
# include <cmath>
# include <cstring>
namespace crude_json {
value::value(value&& other)
: m_Type(other.m_Type)
{
switch (m_Type)
{
case type_t::object: construct(m_Storage, std::move( *object_ptr(other.m_Storage))); break;
case type_t::array: construct(m_Storage, std::move( *array_ptr(other.m_Storage))); break;
case type_t::string: construct(m_Storage, std::move( *string_ptr(other.m_Storage))); break;
case type_t::boolean: construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); break;
case type_t::number: construct(m_Storage, std::move( *number_ptr(other.m_Storage))); break;
default: break;
}
destruct(other.m_Storage, other.m_Type);
other.m_Type = type_t::null;
}
value::value(const value& other)
: m_Type(other.m_Type)
{
switch (m_Type)
{
case type_t::object: construct(m_Storage, *object_ptr(other.m_Storage)); break;
case type_t::array: construct(m_Storage, *array_ptr(other.m_Storage)); break;
case type_t::string: construct(m_Storage, *string_ptr(other.m_Storage)); break;
case type_t::boolean: construct(m_Storage, *boolean_ptr(other.m_Storage)); break;
case type_t::number: construct(m_Storage, *number_ptr(other.m_Storage)); break;
default: break;
}
}
value& value::operator[](size_t index)
{
if (is_null())
m_Type = construct(m_Storage, type_t::array);
if (is_array())
{
auto& v = *array_ptr(m_Storage);
if (index >= v.size())
v.insert(v.end(), index - v.size() + 1, value());
return v[index];
}
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
const value& value::operator[](size_t index) const
{
if (is_array())
return (*array_ptr(m_Storage))[index];
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
value& value::operator[](const string& key)
{
if (is_null())
m_Type = construct(m_Storage, type_t::object);
if (is_object())
return (*object_ptr(m_Storage))[key];
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
const value& value::operator[](const string& key) const
{
if (is_object())
{
auto& o = *object_ptr(m_Storage);
auto it = o.find(key);
CRUDE_ASSERT(it != o.end());
return it->second;
}
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
bool value::contains(const string& key) const
{
if (is_object())
{
auto& o = *object_ptr(m_Storage);
auto it = o.find(key);
return it != o.end();
}
return false;
}
void value::push_back(const value& value)
{
if (is_null())
m_Type = construct(m_Storage, type_t::array);
if (is_array())
{
auto& v = *array_ptr(m_Storage);
v.push_back(value);
}
else
{
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
}
void value::push_back(value&& value)
{
if (is_null())
m_Type = construct(m_Storage, type_t::array);
if (is_array())
{
auto& v = *array_ptr(m_Storage);
v.push_back(std::move(value));
}
else
{
CRUDE_ASSERT(false && "operator[] on unsupported type");
std::terminate();
}
}
void value::swap(value& other)
{
using std::swap;
if (m_Type == other.m_Type)
{
switch (m_Type)
{
case type_t::object: swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); break;
case type_t::array: swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); break;
case type_t::string: swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); break;
case type_t::boolean: swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); break;
case type_t::number: swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); break;
default: break;
}
}
else
{
value tmp(std::move(other));
other.~value();
new (&other) value(std::move(*this));
this->~value();
new (this) value(std::move(tmp));
}
}
string value::dump(const int indent, const char indent_char) const
{
dump_context_t context(indent, indent_char);
context.out.precision(std::numeric_limits<double>::max_digits10 + 1);
context.out << std::defaultfloat;
dump(context, 0);
return context.out.str();
}
void value::dump_context_t::write_indent(int level)
{
if (indent <= 0 || level == 0)
return;
out.fill(indent_char);
out.width(indent * level);
out << indent_char;
out.width(0);
}
void value::dump_context_t::write_separator()
{
if (indent < 0)
return;
out.put(' ');
}
void value::dump_context_t::write_newline()
{
if (indent < 0)
return;
out.put('\n');
}
void value::dump(dump_context_t& context, int level) const
{
context.write_indent(level);
switch (m_Type)
{
case type_t::null:
context.out << "null";
break;
case type_t::object:
context.out << '{';
{
context.write_newline();
bool first = true;
for (auto& entry : *object_ptr(m_Storage))
{
if (!first) { context.out << ','; context.write_newline(); } else first = false;
context.write_indent(level + 1);
context.out << '\"' << entry.first << "\":";
if (!entry.second.is_structured())
{
context.write_separator();
entry.second.dump(context, 0);
}
else
{
context.write_newline();
entry.second.dump(context, level + 1);
}
}
if (!first)
context.write_newline();
}
context.write_indent(level);
context.out << '}';
break;
case type_t::array:
context.out << '[';
{
context.write_newline();
bool first = true;
for (auto& entry : *array_ptr(m_Storage))
{
if (!first) { context.out << ','; context.write_newline(); } else first = false;
if (!entry.is_structured())
{
context.write_indent(level + 1);
entry.dump(context, 0);
}
else
{
entry.dump(context, level + 1);
}
}
if (!first)
context.write_newline();
}
context.write_indent(level);
context.out << ']';
break;
case type_t::string:
context.out << '\"';
if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != string::npos || string_ptr(m_Storage)->find('\0') != string::npos)
{
for (auto c : *string_ptr(m_Storage))
{
if (c == '\"') context.out << "\\\"";
else if (c == '\\') context.out << "\\\\";
else if (c == '/') context.out << "\\/";
else if (c == '\b') context.out << "\\b";
else if (c == '\f') context.out << "\\f";
else if (c == '\n') context.out << "\\n";
else if (c == '\r') context.out << "\\r";
else if (c == '\t') context.out << "\\t";
else if (c == 0) context.out << "\\u0000";
else context.out << c;
}
}
else
context.out << *string_ptr(m_Storage);
context.out << '\"';
break;
case type_t::boolean:
if (*boolean_ptr(m_Storage))
context.out << "true";
else
context.out << "false";
break;
case type_t::number:
context.out << *number_ptr(m_Storage);
break;
default:
break;
}
}
struct value::parser
{
parser(const char* begin, const char* end)
: m_Cursor(begin)
, m_End(end)
{
}
value parse()
{
value v;
// Switch to C locale to make strtod and strtol work as expected
auto previous_locale = std::setlocale(LC_NUMERIC, "C");
// Accept single value only when end of the stream is reached.
if (!accept_element(v) || !eof())
v = value(type_t::discarded);
if (previous_locale && strcmp(previous_locale, "C") != 0)
std::setlocale(LC_NUMERIC, previous_locale);
return v;
}
private:
struct cursor_state
{
cursor_state(parser* p)
: m_Owner(p)
, m_LastCursor(p->m_Cursor)
{
}
void reset()
{
m_Owner->m_Cursor = m_LastCursor;
}
bool operator()(bool accept)
{
if (!accept)
reset();
else
m_LastCursor = m_Owner->m_Cursor;
return accept;
}
private:
parser* m_Owner;
const char* m_LastCursor;
};
cursor_state state()
{
return cursor_state(this);
}
bool accept_value(value& result)
{
return accept_object(result)
|| accept_array(result)
|| accept_string(result)
|| accept_number(result)
|| accept_boolean(result)
|| accept_null(result);
}
bool accept_object(value& result)
{
auto s = state();
object o;
if (s(accept('{') && accept_ws() && accept('}')))
{
result = o;
return true;
}
else if (s(accept('{') && accept_members(o) && accept('}')))
{
result = std::move(o);
return true;
}
return false;
}
bool accept_members(object& o)
{
if (!accept_member(o))
return false;
while (true)
{
auto s = state();
if (!s(accept(',') && accept_member(o)))
break;
}
return true;
}
bool accept_member(object& o)
{
auto s = state();
value key;
value v;
if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && accept_element(v)))
{
o.emplace(std::move(key.get<string>()), std::move(v));
return true;
}
return false;
}
bool accept_array(value& result)
{
auto s = state();
if (s(accept('[') && accept_ws() && accept(']')))
{
result = array();
return true;
}
array a;
if (s(accept('[') && accept_elements(a) && accept(']')))
{
result = std::move(a);
return true;
}
return false;
}
bool accept_elements(array& a)
{
value v;
if (!accept_element(v))
return false;
a.emplace_back(std::move(v));
while (true)
{
auto s = state();
v = nullptr;
if (!s(accept(',') && accept_element(v)))
break;
a.emplace_back(std::move(v));
}
return true;
}
bool accept_element(value& result)
{
auto s = state();
return s(accept_ws() && accept_value(result) && accept_ws());
}
bool accept_string(value& result)
{
auto s = state();
string v;
if (s(accept('\"') && accept_characters(v) && accept('\"')))
{
result = std::move(v);
return true;
}
else
return false;
}
bool accept_characters(string& result)
{
int c;
while (accept_character(c))
{
CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8
result.push_back(static_cast<char>(c));
}
return true;
}
bool accept_character(int& c)
{
auto s = state();
if (accept('\\'))
{
return accept_escape(c);
}
else if (expect('\"'))
return false;
// #todo: Handle UTF-8 sequences.
return s((c = peek()) >= 0) && advance();
}
bool accept_escape(int& c)
{
if (accept('\"')) { c = '\"'; return true; }
if (accept('\\')) { c = '\\'; return true; }
if (accept('/')) { c = '/'; return true; }
if (accept('b')) { c = '\b'; return true; }
if (accept('f')) { c = '\f'; return true; }
if (accept('n')) { c = '\n'; return true; }
if (accept('r')) { c = '\r'; return true; }
if (accept('t')) { c = '\t'; return true; }
auto s = state();
string hex;
hex.reserve(4);
if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && accept_hex(hex) && accept_hex(hex)))
{
char* end = nullptr;
auto v = std::strtol(hex.c_str(), &end, 16);
if (end != hex.c_str() + hex.size())
return false;
c = v;
return true;
}
return false;
}
bool accept_hex(string& result)
{
if (accept_digit(result))
return true;
auto c = peek();
if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
{
advance();
result.push_back(c);
return true;
}
return false;
}
bool accept_number(value& result)
{
auto s = state();
string n;
if (s(accept_int(n) && accept_frac(n) && accept_exp(n)))
{
char* end = nullptr;
auto v = std::strtod(n.c_str(), &end);
if (end != n.c_str() + n.size())
return false;
if (!std::isnormal(v))
return false;
result = v;
return true;
}
return false;
}
bool accept_int(string& result)
{
auto s = state();
string part;
if (s(accept_onenine(part) && accept_digits(part)))
{
result += std::move(part);
return true;
}
part.resize(0);
if (accept_digit(part))
{
result += std::move(part);
return true;
}
part.resize(0);
if (s(accept('-') && accept_onenine(part) && accept_digits(part)))
{
result += '-';
result += std::move(part);
return true;
}
part.resize(0);
if (s(accept('-') && accept_digit(part)))
{
result += '-';
result += std::move(part);
return true;
}
return false;
}
bool accept_digits(string& result)
{
string part;
if (!accept_digit(part))
return false;
while (accept_digit(part))
;
result += std::move(part);
return true;
}
bool accept_digit(string& result)
{
if (accept('0'))
{
result.push_back('0');
return true;
}
else if (accept_onenine(result))
return true;
return false;
}
bool accept_onenine(string& result)
{
auto c = peek();
if (c >= '1' && c <= '9')
{
result.push_back(static_cast<char>(c));
return advance();
}
return false;
}
bool accept_frac(string& result)
{
auto s = state();
string part;
if (s(accept('.') && accept_digits(part)))
{
result += '.';
result += std::move(part);
}
return true;
}
bool accept_exp(string& result)
{
auto s = state();
string part;
if (s(accept('e') && accept_sign(part) && accept_digits(part)))
{
result += 'e';
result += std::move(part);
return true;
}
part.resize(0);
if (s(accept('E') && accept_sign(part) && accept_digits(part)))
{
result += 'E';
result += std::move(part);
}
return true;
}
bool accept_sign(string& result)
{
if (accept('+'))
result.push_back('+');
else if (accept('-'))
result.push_back('-');
return true;
}
bool accept_ws()
{
while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20'))
advance();
return true;
}
bool accept_boolean(value& result)
{
if (accept("true"))
{
result = true;
return true;
}
else if (accept("false"))
{
result = false;
return true;
}
return false;
}
bool accept_null(value& result)
{
if (accept("null"))
{
result = nullptr;
return true;
}
return false;
}
bool accept(char c)
{
if (expect(c))
return advance();
else
return false;
}
bool accept(const char* str)
{
auto last = m_Cursor;
while (*str)
{
if (eof() || *str != *m_Cursor)
{
m_Cursor = last;
return false;
}
advance();
++str;
}
return true;
}
int peek() const
{
if (!eof())
return *m_Cursor;
else
return -1;
}
bool expect(char c)
{
return peek() == c;
}
bool advance(int count = 1)
{
if (m_Cursor + count > m_End)
{
m_Cursor = m_End;
return false;
}
m_Cursor += count;
return true;
}
bool eof() const
{
return m_Cursor == m_End;
}
const char* m_Cursor;
const char* m_End;
};
value value::parse(const string& data)
{
auto p = parser(data.c_str(), data.c_str() + data.size());
auto v = p.parse();
return v;
}
} // namespace crude_json
+223
View File
@@ -0,0 +1,223 @@
// Crude implementation of JSON value object and parser.
//
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
# ifndef __CRUDE_JSON_H__
# define __CRUDE_JSON_H__
# pragma once
# include <type_traits>
# include <string>
# include <vector>
# include <map>
# include <cstddef>
# include <algorithm>
# include <sstream>
# ifndef CRUDE_ASSERT
# include <cassert>
# define CRUDE_ASSERT(expr) assert(expr)
# endif
namespace crude_json {
struct value;
using string = std::string;
using object = std::map<string, value>;
using array = std::vector<value>;
using number = double;
using boolean = bool;
using null = std::nullptr_t;
enum class type_t
{
null,
object,
array,
string,
boolean,
number,
discarded
};
struct value
{
value(type_t type = type_t::null): m_Type(construct(m_Storage, type)) {}
value(value&& other);
value(const value& other);
value( null) : m_Type(construct(m_Storage, null())) {}
value( object&& v): m_Type(construct(m_Storage, std::move(v))) {}
value(const object& v): m_Type(construct(m_Storage, v)) {}
value( array&& v): m_Type(construct(m_Storage, std::move(v))) {}
value(const array& v): m_Type(construct(m_Storage, v)) {}
value( string&& v): m_Type(construct(m_Storage, std::move(v))) {}
value(const string& v): m_Type(construct(m_Storage, v)) {}
value(const char* v): m_Type(construct(m_Storage, v)) {}
value( boolean v): m_Type(construct(m_Storage, v)) {}
value( number v): m_Type(construct(m_Storage, v)) {}
~value() { destruct(m_Storage, m_Type); }
value& operator=(value&& other) { if (this != &other) { value(std::move(other)).swap(*this); } return *this; }
value& operator=(const value& other) { if (this != &other) { value( other).swap(*this); } return *this; }
value& operator=( null) { auto other = value( ); swap(other); return *this; }
value& operator=( object&& v) { auto other = value(std::move(v)); swap(other); return *this; }
value& operator=(const object& v) { auto other = value( v); swap(other); return *this; }
value& operator=( array&& v) { auto other = value(std::move(v)); swap(other); return *this; }
value& operator=(const array& v) { auto other = value( v); swap(other); return *this; }
value& operator=( string&& v) { auto other = value(std::move(v)); swap(other); return *this; }
value& operator=(const string& v) { auto other = value( v); swap(other); return *this; }
value& operator=(const char* v) { auto other = value( v); swap(other); return *this; }
value& operator=( boolean v) { auto other = value( v); swap(other); return *this; }
value& operator=( number v) { auto other = value( v); swap(other); return *this; }
type_t type() const { return m_Type; }
operator type_t() const { return m_Type; }
value& operator[](size_t index);
const value& operator[](size_t index) const;
value& operator[](const string& key);
const value& operator[](const string& key) const;
bool contains(const string& key) const;
void push_back(const value& value);
void push_back(value&& value);
bool is_primitive() const { return is_string() || is_number() || is_boolean() || is_null(); }
bool is_structured() const { return is_object() || is_array(); }
bool is_null() const { return m_Type == type_t::null; }
bool is_object() const { return m_Type == type_t::object; }
bool is_array() const { return m_Type == type_t::array; }
bool is_string() const { return m_Type == type_t::string; }
bool is_boolean() const { return m_Type == type_t::boolean; }
bool is_number() const { return m_Type == type_t::number; }
bool is_discarded() const { return m_Type == type_t::discarded; }
template <typename T> const T& get() const;
template <typename T> T& get();
string dump(const int indent = -1, const char indent_char = ' ') const;
void swap(value& other);
inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); }
// Returns discarded value for invalid inputs.
static value parse(const string& data);
private:
struct parser;
// VS2015: std::max() is not constexpr yet.
# define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a))
# define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c)
# define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d)
# define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e)
enum
{
max_size = CRUDE_MAX5( sizeof(string), sizeof(object), sizeof(array), sizeof(number), sizeof(boolean)),
max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), alignof(number), alignof(boolean))
};
# undef CRUDE_MAX5
# undef CRUDE_MAX4
# undef CRUDE_MAX3
# undef CRUDE_MAX2
using storage_t = std::aligned_storage<max_size, max_align>::type;
static object* object_ptr( storage_t& storage) { return reinterpret_cast< object*>(&storage); }
static const object* object_ptr(const storage_t& storage) { return reinterpret_cast<const object*>(&storage); }
static array* array_ptr( storage_t& storage) { return reinterpret_cast< array*>(&storage); }
static const array* array_ptr(const storage_t& storage) { return reinterpret_cast<const array*>(&storage); }
static string* string_ptr( storage_t& storage) { return reinterpret_cast< string*>(&storage); }
static const string* string_ptr(const storage_t& storage) { return reinterpret_cast<const string*>(&storage); }
static boolean* boolean_ptr( storage_t& storage) { return reinterpret_cast< boolean*>(&storage); }
static const boolean* boolean_ptr(const storage_t& storage) { return reinterpret_cast<const boolean*>(&storage); }
static number* number_ptr( storage_t& storage) { return reinterpret_cast< number*>(&storage); }
static const number* number_ptr(const storage_t& storage) { return reinterpret_cast<const number*>(&storage); }
static type_t construct(storage_t& storage, type_t type)
{
switch (type)
{
case type_t::object: new (&storage) object(); break;
case type_t::array: new (&storage) array(); break;
case type_t::string: new (&storage) string(); break;
case type_t::boolean: new (&storage) boolean(); break;
case type_t::number: new (&storage) number(); break;
default: break;
}
return type;
}
static type_t construct(storage_t& storage, null) { return type_t::null; }
static type_t construct(storage_t& storage, object&& value) { new (&storage) object(std::forward<object>(value)); return type_t::object; }
static type_t construct(storage_t& storage, const object& value) { new (&storage) object(value); return type_t::object; }
static type_t construct(storage_t& storage, array&& value) { new (&storage) array(std::forward<array>(value)); return type_t::array; }
static type_t construct(storage_t& storage, const array& value) { new (&storage) array(value); return type_t::array; }
static type_t construct(storage_t& storage, string&& value) { new (&storage) string(std::forward<string>(value)); return type_t::string; }
static type_t construct(storage_t& storage, const string& value) { new (&storage) string(value); return type_t::string; }
static type_t construct(storage_t& storage, const char* value) { new (&storage) string(value); return type_t::string; }
static type_t construct(storage_t& storage, boolean value) { new (&storage) boolean(value); return type_t::boolean; }
static type_t construct(storage_t& storage, number value) { new (&storage) number(value); return type_t::number; }
static void destruct(storage_t& storage, type_t type)
{
switch (type)
{
case type_t::object: object_ptr(storage)->~object(); break;
case type_t::array: array_ptr(storage)->~array(); break;
case type_t::string: string_ptr(storage)->~string(); break;
default: break;
}
}
struct dump_context_t
{
std::ostringstream out;
const int indent = -1;
const char indent_char = ' ';
// VS2015: Aggregate initialization isn't a thing yet.
dump_context_t(const int indent, const char indent_char)
: indent(indent)
, indent_char(indent_char)
{
}
void write_indent(int level);
void write_separator();
void write_newline();
};
void dump(dump_context_t& context, int level) const;
storage_t m_Storage;
type_t m_Type;
};
template <> inline const object& value::get<object>() const { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
template <> inline const array& value::get<array>() const { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
template <> inline const string& value::get<string>() const { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
template <> inline const boolean& value::get<boolean>() const { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
template <> inline const number& value::get<number>() const { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
template <> inline object& value::get<object>() { CRUDE_ASSERT(m_Type == type_t::object); return *object_ptr(m_Storage); }
template <> inline array& value::get<array>() { CRUDE_ASSERT(m_Type == type_t::array); return *array_ptr(m_Storage); }
template <> inline string& value::get<string>() { CRUDE_ASSERT(m_Type == type_t::string); return *string_ptr(m_Storage); }
template <> inline boolean& value::get<boolean>() { CRUDE_ASSERT(m_Type == type_t::boolean); return *boolean_ptr(m_Storage); }
template <> inline number& value::get<number>() { CRUDE_ASSERT(m_Type == type_t::number); return *number_ptr(m_Storage); }
} // namespace crude_json
# endif // __CRUDE_JSON_H__
@@ -0,0 +1,142 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_BEZIER_MATH_H__
# define __IMGUI_BEZIER_MATH_H__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_extra_math.h"
//------------------------------------------------------------------------------
template <typename T>
struct ImCubicBezierPointsT
{
T P0;
T P1;
T P2;
T P3;
};
using ImCubicBezierPoints = ImCubicBezierPointsT<ImVec2>;
//------------------------------------------------------------------------------
// Low-level Bezier curve sampling.
template <typename T> inline T ImLinearBezier(const T& p0, const T& p1, float t);
template <typename T> inline T ImLinearBezierDt(const T& p0, const T& p1, float t);
template <typename T> inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t);
template <typename T> inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t);
template <typename T> inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t);
template <typename T> inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t);
// High-level Bezier sampling, automatically collapse to lower level Bezier curves if control points overlap.
template <typename T> inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t);
template <typename T> inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t);
template <typename T> inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t);
template <typename T> inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t);
// Calculate approximate length of Cubic Bezier curve.
template <typename T> inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3);
template <typename T> inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve);
// Splits Cubic Bezier curve into two curves.
template <typename T>
struct ImCubicBezierSplitResultT
{
ImCubicBezierPointsT<T> Left;
ImCubicBezierPointsT<T> Right;
};
using ImCubicBezierSplitResult = ImCubicBezierSplitResultT<ImVec2>;
template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t);
template <typename T> inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t);
// Returns bounding rectangle of Cubic Bezier curve.
inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3);
inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve);
// Project point on Cubic Bezier curve.
struct ImProjectResult
{
ImVec2 Point; // Point on curve
float Time; // [0 - 1]
float Distance; // Distance to curve
};
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions = 100);
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions = 100);
// Calculate intersection between line and a Cubic Bezier curve.
struct ImCubicBezierIntersectResult
{
int Count;
ImVec2 Points[3];
};
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1);
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line);
// Adaptive Cubic Bezier subdivision.
enum ImCubicBezierSubdivideFlags
{
ImCubicBezierSubdivide_None = 0,
ImCubicBezierSubdivide_SkipFirst = 1
};
struct ImCubicBezierSubdivideSample
{
ImVec2 Point;
ImVec2 Tangent;
};
using ImCubicBezierSubdivideCallback = void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer);
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
// F has signature void(const ImCubicBezierSubdivideSample& p)
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None);
// Fixed step Cubic Bezier subdivision.
struct ImCubicBezierFixedStepSample
{
float T;
float Length;
ImVec2 Point;
bool BreakSearch;
};
using ImCubicBezierFixedStepCallback = void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer);
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
// F has signature void(const ImCubicBezierFixedStepSample& p)
template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
template <typename F> inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot = false, float max_value_error = 1e-3f, float max_t_error = 1e-5f);
//------------------------------------------------------------------------------
# include "imgui_bezier_math.inl"
//------------------------------------------------------------------------------
# endif // __IMGUI_BEZIER_MATH_H__
@@ -0,0 +1,668 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_BEZIER_MATH_INL__
# define __IMGUI_BEZIER_MATH_INL__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_bezier_math.h"
# include <map> // used in ImCubicBezierFixedStep
//------------------------------------------------------------------------------
template <typename T>
inline T ImLinearBezier(const T& p0, const T& p1, float t)
{
return p0 + t * (p1 - p0);
}
template <typename T>
inline T ImLinearBezierDt(const T& p0, const T& p1, float t)
{
return p1 - p0;
}
template <typename T>
inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t)
{
const auto a = 1 - t;
return a * a * p0 + 2 * t * a * p1 + t * t * p2;
}
template <typename T>
inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t)
{
return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1);
}
template <typename T>
inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t)
{
const auto a = 1 - t;
const auto b = a * a * a;
const auto c = t * t * t;
return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3;
}
template <typename T>
inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t)
{
const auto a = 1 - t;
const auto b = a * a;
const auto c = t * t;
const auto d = 2 * t * a;
return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c;
}
template <typename T>
inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t)
{
const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
if (cp0_zero && cp1_zero)
return ImLinearBezier(p0, p3, t);
else if (cp0_zero)
return ImQuadraticBezier(p0, p2, p3, t);
else if (cp1_zero)
return ImQuadraticBezier(p0, p1, p3, t);
else
return ImCubicBezier(p0, p1, p2, p3, t);
}
template <typename T>
inline T ImCubicBezierSample(const ImCubicBezierPointsT<T>& curve, float t)
{
return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t);
}
template <typename T>
inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t)
{
const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f;
const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f;
if (cp0_zero && cp1_zero)
return ImLinearBezierDt(p0, p3, t);
else if (cp0_zero)
return ImQuadraticBezierDt(p0, p2, p3, t);
else if (cp1_zero)
return ImQuadraticBezierDt(p0, p1, p3, t);
else
return ImCubicBezierDt(p0, p1, p2, p3, t);
}
template <typename T>
inline T ImCubicBezierTangent(const ImCubicBezierPointsT<T>& curve, float t)
{
return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t);
}
template <typename T>
inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3)
{
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
static const float t_values[] =
{
-0.0640568928626056260850430826247450385909f,
0.0640568928626056260850430826247450385909f,
-0.1911188674736163091586398207570696318404f,
0.1911188674736163091586398207570696318404f,
-0.3150426796961633743867932913198102407864f,
0.3150426796961633743867932913198102407864f,
-0.4337935076260451384870842319133497124524f,
0.4337935076260451384870842319133497124524f,
-0.5454214713888395356583756172183723700107f,
0.5454214713888395356583756172183723700107f,
-0.6480936519369755692524957869107476266696f,
0.6480936519369755692524957869107476266696f,
-0.7401241915785543642438281030999784255232f,
0.7401241915785543642438281030999784255232f,
-0.8200019859739029219539498726697452080761f,
0.8200019859739029219539498726697452080761f,
-0.8864155270044010342131543419821967550873f,
0.8864155270044010342131543419821967550873f,
-0.9382745520027327585236490017087214496548f,
0.9382745520027327585236490017087214496548f,
-0.9747285559713094981983919930081690617411f,
0.9747285559713094981983919930081690617411f,
-0.9951872199970213601799974097007368118745f,
0.9951872199970213601799974097007368118745f
};
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
static const float c_values[] =
{
0.1279381953467521569740561652246953718517f,
0.1279381953467521569740561652246953718517f,
0.1258374563468282961213753825111836887264f,
0.1258374563468282961213753825111836887264f,
0.1216704729278033912044631534762624256070f,
0.1216704729278033912044631534762624256070f,
0.1155056680537256013533444839067835598622f,
0.1155056680537256013533444839067835598622f,
0.1074442701159656347825773424466062227946f,
0.1074442701159656347825773424466062227946f,
0.0976186521041138882698806644642471544279f,
0.0976186521041138882698806644642471544279f,
0.0861901615319532759171852029837426671850f,
0.0861901615319532759171852029837426671850f,
0.0733464814110803057340336152531165181193f,
0.0733464814110803057340336152531165181193f,
0.0592985849154367807463677585001085845412f,
0.0592985849154367807463677585001085845412f,
0.0442774388174198061686027482113382288593f,
0.0442774388174198061686027482113382288593f,
0.0285313886289336631813078159518782864491f,
0.0285313886289336631813078159518782864491f,
0.0123412297999871995468056670700372915759f,
0.0123412297999871995468056670700372915759f
};
static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), "");
auto arc = [p0, p1, p2, p3](float t)
{
const auto p = ImCubicBezierDt(p0, p1, p2, p3, t);
const auto l = ImLength(p);
return l;
};
const auto z = 0.5f;
const auto n = sizeof(t_values) / sizeof(*t_values);
auto accumulator = 0.0f;
for (size_t i = 0; i < n; ++i)
{
const auto t = z * t_values[i] + z;
accumulator += c_values[i] * arc(t);
}
return z * accumulator;
}
template <typename T>
inline float ImCubicBezierLength(const ImCubicBezierPointsT<T>& curve)
{
return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3);
}
template <typename T>
inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t)
{
const auto z1 = t;
const auto z2 = z1 * z1;
const auto z3 = z1 * z1 * z1;
const auto s1 = z1 - 1;
const auto s2 = s1 * s1;
const auto s3 = s1 * s1 * s1;
return ImCubicBezierSplitResultT<T>
{
ImCubicBezierPointsT<T>
{
p0,
z1 * p1 - s1 * p0,
z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0,
z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0
},
ImCubicBezierPointsT<T>
{
z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3,
z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3,
z1 * p2 - s1 * p3,
p3,
}
};
}
template <typename T>
inline ImCubicBezierSplitResultT<T> ImCubicBezierSplit(const ImCubicBezierPointsT<T>& curve, float t)
{
return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t);
}
inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3)
{
auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0;
auto b = 6 * p0 - 12 * p1 + 6 * p2;
auto c = 3 * p1 - 3 * p0;
auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c);
auto tl = ImMin(p0, p3);
auto rb = ImMax(p0, p3);
# define IM_VEC2_INDEX(v, i) *(&v.x + i)
for (int i = 0; i < 2; ++i)
{
if (IM_VEC2_INDEX(delta_squared, i) >= 0)
{
auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i));
auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i));
if (t0 > 0 && t0 < 1)
{
auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0);
IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
}
auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i));
if (t1 > 0 && t1 < 1)
{
auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1);
IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p);
IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p);
}
}
}
# undef IM_VEC2_INDEX
return ImRect(tl, rb);
}
inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve)
{
return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3);
}
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions)
{
// http://pomax.github.io/bezierinfo/#projections
const float epsilon = 1e-5f;
const float fixed_step = 1.0f / static_cast<float>(subdivisions - 1);
ImProjectResult result;
result.Point = point;
result.Time = 0.0f;
result.Distance = FLT_MAX;
// Step 1: Coarse check
for (int i = 0; i < subdivisions; ++i)
{
auto t = i * fixed_step;
auto p = ImCubicBezier(p0, p1, p2, p3, t);
auto s = point - p;
auto d = ImDot(s, s);
if (d < result.Distance)
{
result.Point = p;
result.Time = t;
result.Distance = d;
}
}
if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon)
{
result.Distance = ImSqrt(result.Distance);
return result;
}
// Step 2: Fine check
auto left = result.Time - fixed_step;
auto right = result.Time + fixed_step;
auto step = fixed_step * 0.1f;
for (auto t = left; t < right + step; t += step)
{
auto p = ImCubicBezier(p0, p1, p2, p3, t);
auto s = point - p;
auto d = ImDot(s, s);
if (d < result.Distance)
{
result.Point = p;
result.Time = t;
result.Distance = d;
}
}
result.Distance = ImSqrt(result.Distance);
return result;
}
inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions)
{
return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions);
}
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1)
{
auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int
{
int count = 0;
auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; };
auto A = b / a;
auto B = c / a;
auto C = d / a;
auto Q = (3 * B - ImPow(A, 2)) / 9;
auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54;
auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant
if (D >= 0) // complex or duplicate roots
{
auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f));
auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f));
roots[0] = -A / 3 + (S + T); // real root
roots[1] = -A / 3 - (S + T) / 2; // real part of complex root
roots[2] = -A / 3 - (S + T) / 2; // real part of complex root
auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair
// discard complex roots
if (Im != 0)
count = 1;
else
count = 3;
}
else // distinct real roots
{
auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3)));
roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3;
roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3;
roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3;
count = 3;
}
return count;
};
// https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as
//
// Start with Bezier using Bernstein polynomials for weighting functions:
// (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3
//
// Expand and collect terms to form linear combinations of original Bezier
// controls. This ends up with a vector cubic in t:
// (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0
// /\ /\ /\ /\
// || || || ||
// c3 c2 c1 c0
// Calculate the coefficients
auto c3 = -p0 + 3 * p1 - 3 * p2 + p3;
auto c2 = 3 * p0 - 6 * p1 + 3 * p2;
auto c1 = -3 * p0 + 3 * p1;
auto c0 = p0;
// Convert line to normal form: ax + by + c = 0
auto a = a1.y - a0.y;
auto b = a0.x - a1.x;
auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x);
// Rotate each cubic coefficient using line for new coordinate system?
// Find roots of rotated cubic
float roots[3];
auto rootCount = cubic_roots(
a * c3.x + b * c3.y,
a * c2.x + b * c2.y,
a * c1.x + b * c1.y,
a * c0.x + b * c0.y + c,
roots);
// Any roots in closed interval [0,1] are intersections on Bezier, but
// might not be on the line segment.
// Find intersections and calculate point coordinates
auto min = ImMin(a0, a1);
auto max = ImMax(a0, a1);
ImCubicBezierIntersectResult result;
auto points = result.Points;
for (int i = 0; i < rootCount; ++i)
{
auto root = roots[i];
if (0 <= root && root <= 1)
{
// We're within the Bezier curve
// Find point on Bezier
auto p = ImCubicBezier(p0, p1, p2, p3, root);
// See if point is on line segment
// Had to make special cases for vertical and horizontal lines due
// to slight errors in calculation of p00
if (a0.x == a1.x)
{
if (min.y <= p.y && p.y <= max.y)
*points++ = p;
}
else if (a0.y == a1.y)
{
if (min.x <= p.x && p.x <= max.x)
*points++ = p;
}
else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y)
{
*points++ = p;
}
}
}
result.Count = static_cast<int>(points - result.Points);
return result;
}
inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line)
{
return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B);
}
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
{
return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
}
inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
{
struct Tesselator
{
ImCubicBezierSubdivideCallback Callback;
void* UserPointer;
float TesselationTollerance;
ImCubicBezierSubdivideFlags Flags;
void Commit(const ImVec2& p, const ImVec2& t)
{
ImCubicBezierSubdivideSample sample;
sample.Point = p;
sample.Tangent = t;
Callback(sample, UserPointer);
}
void Subdivide(const ImCubicBezierPoints& curve, int level = 0)
{
float dx = curve.P3.x - curve.P0.x;
float dy = curve.P3.y - curve.P0.y;
float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx);
float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx);
d2 = (d2 >= 0) ? d2 : -d2;
d3 = (d3 >= 0) ? d3 : -d3;
if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy))
{
Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f));
}
else if (level < 10)
{
const auto p12 = (curve.P0 + curve.P1) * 0.5f;
const auto p23 = (curve.P1 + curve.P2) * 0.5f;
const auto p34 = (curve.P2 + curve.P3) * 0.5f;
const auto p123 = (p12 + p23) * 0.5f;
const auto p234 = (p23 + p34) * 0.5f;
const auto p1234 = (p123 + p234) * 0.5f;
Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1);
Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1);
}
}
};
if (tess_tol < 0)
tess_tol = 1.118f; // sqrtf(1.25f)
Tesselator tesselator;
tesselator.Callback = callback;
tesselator.UserPointer = user_pointer;
tesselator.TesselationTollerance = tess_tol * tess_tol;
tesselator.Flags = flags;
if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst))
tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f));
tesselator.Subdivide(curve, 0);
}
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags)
{
auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
{
auto& callback = *reinterpret_cast<F*>(user_pointer);
callback(p);
};
ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags);
}
template <typename F> inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags)
{
auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer)
{
auto& callback = *reinterpret_cast<F*>(user_pointer);
callback(p);
};
ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags);
}
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
{
if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0)
return;
ImCubicBezierFixedStepSample sample;
sample.T = 0.0f;
sample.Length = 0.0f;
sample.Point = p0;
sample.BreakSearch = false;
callback(sample, user_pointer);
if (sample.BreakSearch)
return;
const auto length = ImCubicBezierLength(p0, p1, p2, p3);
const auto point_count = static_cast<int>(length / step) + (overshoot ? 2 : 1);
const auto t_min = 0.0f;
const auto t_max = step * point_count / length;
const auto t_0 = (t_min + t_max) * 0.5f;
// #todo: replace map with ImVector + binary search
std::map<float, float> cache;
for (int point_index = 1; point_index < point_count; ++point_index)
{
const auto targetLength = point_index * step;
float t_start = t_min;
float t_end = t_max;
float t = t_0;
float t_best = t;
float error_best = length;
while (true)
{
auto cacheIt = cache.find(t);
if (cacheIt == cache.end())
{
const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left;
const auto length = ImCubicBezierLength(front);
cacheIt = cache.emplace(t, length).first;
}
const auto length = cacheIt->second;
const auto error = targetLength - length;
if (error < error_best)
{
error_best = error;
t_best = t;
}
if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error)
{
sample.T = t;
sample.Length = length;
sample.Point = ImCubicBezier(p0, p1, p2, p3, t);
callback(sample, user_pointer);
if (sample.BreakSearch)
return;
break;
}
else if (error < 0.0f)
t_end = t;
else // if (error > 0.0f)
t_start = t;
t = (t_start + t_end) * 0.5f;
}
}
}
inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
{
ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
}
// F has signature void(const ImCubicBezierFixedStepSample& p)
template <typename F>
inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error)
{
auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
{
auto& callback = *reinterpret_cast<F*>(user_pointer);
callback(sample);
};
ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error);
}
template <typename F>
inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error)
{
auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer)
{
auto& callback = *reinterpret_cast<F*>(user_pointer);
callback(sample);
};
ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error);
}
//------------------------------------------------------------------------------
# endif // __IMGUI_BEZIER_MATH_INL__
+526
View File
@@ -0,0 +1,526 @@
# define IMGUI_DEFINE_MATH_OPERATORS
# include "imgui_canvas.h"
# include <type_traits>
// https://stackoverflow.com/a/36079786
# define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \
\
template <typename __boost_has_member_T__> \
class __trait_name__ \
{ \
using check_type = ::std::remove_const_t<__boost_has_member_T__>; \
struct no_type {char x[2];}; \
using yes_type = char; \
\
struct base { void __member_name__() {}}; \
struct mixin : public base, public check_type {}; \
\
template <void (base::*)()> struct aux {}; \
\
template <typename U> static no_type test(aux<&U::__member_name__>*); \
template <typename U> static yes_type test(...); \
\
public: \
\
static constexpr bool value = (sizeof(yes_type) == sizeof(test<mixin>(0))); \
}
namespace ImCanvasDetails {
DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale);
struct FringeScaleRef
{
// Overload is present when ImDrawList does have _FringeScale member variable.
template <typename T>
static float& Get(typename std::enable_if<HasFringeScale<T>::value, T>::type* drawList)
{
return drawList->_FringeScale;
}
// Overload is present when ImDrawList does not have _FringeScale member variable.
template <typename T>
static float& Get(typename std::enable_if<!HasFringeScale<T>::value, T>::type*)
{
static float placeholder = 1.0f;
return placeholder;
}
};
} // namespace ImCanvasDetails
// Returns a reference to _FringeScale extension to ImDrawList
//
// If ImDrawList does not have _FringeScale a placeholder is returned.
static inline float& ImFringeScaleRef(ImDrawList* drawList)
{
using namespace ImCanvasDetails;
return FringeScaleRef::Get<ImDrawList>(drawList);
}
static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); }
bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size)
{
return Begin(ImGui::GetID(id), size);
}
bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size)
{
IM_ASSERT(m_InBeginEnd == false);
m_WidgetPosition = ImGui::GetCursorScreenPos();
m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail());
m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize);
m_DrawList = ImGui::GetWindowDrawList();
UpdateViewTransformPosition();
if (ImGui::IsClippedEx(m_WidgetRect, id)) // IsClippedEx dropped its 3rd arg upstream in ImGui
return false;
// Save current channel, so we can assert when user
// call canvas API with different one.
m_ExpectedChannel = m_DrawList->_Splitter._Current;
// #debug: Canvas content.
//m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, IM_COL32(0, 0, 0, 64));
m_DrawList->AddRect(m_WidgetRect.Min, m_WidgetRect.Max, IM_COL32(255, 0, 255, 64));
ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
# if IMGUI_EX_CANVAS_DEFERED()
m_Ranges.resize(0);
# endif
SaveInputState();
SaveViewportState();
EnterLocalSpace();
// Emit dummy widget matching bounds of the canvas.
ImGui::SetCursorScreenPos(m_ViewRect.Min);
ImGui::Dummy(m_ViewRect.GetSize());
ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f));
m_InBeginEnd = true;
return true;
}
void ImGuiEx::Canvas::End()
{
// If you're here your call to Begin() returned false,
// or Begin() wasn't called at all.
IM_ASSERT(m_InBeginEnd == true);
// If you're here, please make sure you do not interleave
// channel splitter with canvas.
// Always call canvas function with using same channel.
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
//auto& io = ImGui::GetIO();
// Check: Unmatched calls to Suspend() / Resume(). Please check your code.
IM_ASSERT(m_SuspendCounter == 0);
LeaveLocalSpace();
// Emit dummy widget matching bounds of the canvas.
ImGui::SetCursorScreenPos(m_WidgetPosition);
ImGui::Dummy(m_WidgetSize);
// #debug: Rect around canvas. Content should be inside these bounds.
//m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255));
m_InBeginEnd = false;
}
void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale)
{
SetView(CanvasView(origin, scale));
}
void ImGuiEx::Canvas::SetView(const CanvasView& view)
{
if (m_InBeginEnd)
LeaveLocalSpace();
if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y)
{
m_View.Origin = view.Origin;
UpdateViewTransformPosition();
}
if (m_View.Scale != view.Scale)
{
m_View.Scale = view.Scale;
m_View.InvScale = view.InvScale;
}
if (m_InBeginEnd)
EnterLocalSpace();
}
void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint)
{
auto view = CalcCenterView(canvasPoint);
SetView(view);
}
ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImVec2& canvasPoint) const
{
auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f);
auto localOffset = canvasPoint - localCenter;
auto offset = FromLocalV(localOffset);
return CanvasView{ m_View.Origin - offset, m_View.Scale };
}
void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect)
{
auto view = CalcCenterView(canvasRect);
SetView(view);
}
ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView(const ImRect& canvasRect) const
{
auto canvasRectSize = canvasRect.GetSize();
if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f)
return View();
auto widgetAspectRatio = m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f;
auto canvasRectAspectRatio = canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f;
if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f)
return View();
auto newOrigin = m_View.Origin;
auto newScale = m_View.Scale;
if (canvasRectAspectRatio > widgetAspectRatio)
{
// width span across view
newScale = m_WidgetSize.x / canvasRectSize.x;
newOrigin = canvasRect.Min * -newScale;
newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f;
}
else
{
// height span across view
newScale = m_WidgetSize.y / canvasRectSize.y;
newOrigin = canvasRect.Min * -newScale;
newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f;
}
return CanvasView{ newOrigin, newScale };
}
void ImGuiEx::Canvas::Suspend()
{
// If you're here, please make sure you do not interleave
// channel splitter with canvas.
// Always call canvas function with using same channel.
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
if (m_SuspendCounter == 0)
LeaveLocalSpace();
++m_SuspendCounter;
}
void ImGuiEx::Canvas::Resume()
{
// If you're here, please make sure you do not interleave
// channel splitter with canvas.
// Always call canvas function with using same channel.
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
// Check: Number of calls to Resume() do not match calls to Suspend(). Please check your code.
IM_ASSERT(m_SuspendCounter > 0);
if (--m_SuspendCounter == 0)
EnterLocalSpace();
}
ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const
{
return point * m_View.Scale + m_ViewTransformPosition;
}
ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, const CanvasView& view) const
{
return point * view.Scale + view.Origin + m_WidgetPosition;
}
ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const
{
return vector * m_View.Scale;
}
ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, const CanvasView& view) const
{
return vector * view.Scale;
}
ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const
{
return (point - m_ViewTransformPosition) * m_View.InvScale;
}
ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, const CanvasView& view) const
{
return (point - view.Origin - m_WidgetPosition) * view.InvScale;
}
ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const
{
return vector * m_View.InvScale;
}
ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, const CanvasView& view) const
{
return vector * view.InvScale;
}
ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const
{
ImRect result;
result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale;
result.Max = (m_WidgetSize - view.Origin) * view.InvScale;
return result;
}
void ImGuiEx::Canvas::UpdateViewTransformPosition()
{
m_ViewTransformPosition = m_View.Origin + m_WidgetPosition;
}
void ImGuiEx::Canvas::SaveInputState()
{
auto& io = ImGui::GetIO();
m_MousePosBackup = io.MousePos;
m_MousePosPrevBackup = io.MousePosPrev;
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
m_MouseClickedPosBackup[i] = io.MouseClickedPos[i];
// Record cursor max to prevent scrollbars from appearing.
m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos;
}
void ImGuiEx::Canvas::RestoreInputState()
{
auto& io = ImGui::GetIO();
io.MousePos = m_MousePosBackup;
io.MousePosPrev = m_MousePosPrevBackup;
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
io.MouseClickedPos[i] = m_MouseClickedPosBackup[i];
ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
}
// UsdLayerManager local patch, ported from upstream imgui-node-editor
// (see the declaration in imgui_canvas.h for why).
void ImGuiEx::Canvas::SaveViewportState()
{
# if defined(IMGUI_HAS_VIEWPORT)
auto window = ImGui::GetCurrentWindow();
auto viewport = ImGui::GetWindowViewport();
m_WindowPosBackup = window->Pos;
m_ViewportPosBackup = viewport->Pos;
m_ViewportSizeBackup = viewport->Size;
# if IMGUI_VERSION_NUM > 18002
m_ViewportWorkPosBackup = viewport->WorkPos;
m_ViewportWorkSizeBackup = viewport->WorkSize;
# else
m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin;
m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax;
# endif
# endif
}
void ImGuiEx::Canvas::RestoreViewportState()
{
# if defined(IMGUI_HAS_VIEWPORT)
auto window = ImGui::GetCurrentWindow();
auto viewport = ImGui::GetWindowViewport();
window->Pos = m_WindowPosBackup;
viewport->Pos = m_ViewportPosBackup;
viewport->Size = m_ViewportSizeBackup;
# if IMGUI_VERSION_NUM > 18002
viewport->WorkPos = m_ViewportWorkPosBackup;
viewport->WorkSize = m_ViewportWorkSizeBackup;
# else
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup;
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup;
# endif
# endif
}
void ImGuiEx::Canvas::EnterLocalSpace()
{
// Prepare ImDrawList for drawing in local coordinate system:
// - determine visible part of the canvas
// - start unique draw command
// - add clip rect matching canvas size
// - record current command index
// - record current vertex write index
// Determine visible part of the canvas. Make it before
// adding new command, to avoid round rip where command
// is removed in PopClipRect() and added again next PushClipRect().
ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true);
auto clipped_clip_rect = m_DrawList->_ClipRectStack.back();
ImGui::PopClipRect();
// Make sure we do not share draw command with anyone. We don't want to mess
// with someones clip rectangle.
// #FIXME:
// This condition is not enough to avoid when user choose
// to use channel splitter.
//
// To deal with Suspend()/Resume() calls empty draw command
// is always added then splitter is active. Otherwise
// channel merger will collapse our draw command one with
// different clip rectangle.
//
// More investigation is needed. To get to the bottom of this.
if ((!m_DrawList->CmdBuffer.empty() && m_DrawList->CmdBuffer.back().ElemCount > 0) || m_DrawList->_Splitter._Count > 1)
m_DrawList->AddDrawCmd();
# if IMGUI_EX_CANVAS_DEFERED()
m_Ranges.resize(m_Ranges.Size + 1);
m_CurrentRange = &m_Ranges.back();
m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
m_CurrentRange->BeginVertexIndex = m_DrawList->_VtxCurrentIdx;
# endif
m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size - 1, 0);
m_DrawListStartVertexIndex = m_DrawList->_VtxCurrentIdx;
// Clip rectangle in parent canvas space and move it to local space.
clipped_clip_rect.x = (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale;
clipped_clip_rect.y = (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale;
clipped_clip_rect.z = (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale;
clipped_clip_rect.w = (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale;
ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false);
// Transform mouse position to local space.
auto& io = ImGui::GetIO();
io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale;
io.MousePosPrev = (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale;
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
// move the viewport rect into local space too, so IsMouseHoveringRect's
// screen-space viewport overlap check (docking-branch ImGui) passes for
// items at canvas coordinates outside the screen range (e.g. negative).
// See SaveViewportState() declaration for the full story.
# if defined(IMGUI_HAS_VIEWPORT)
{
auto window = ImGui::GetCurrentWindow();
window->Pos = ImVec2(0.0f, 0.0f);
auto viewport_min = m_ViewportPosBackup;
auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup;
viewport_min.x = (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale;
viewport_min.y = (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale;
viewport_max.x = (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale;
viewport_max.y = (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale;
auto viewport = ImGui::GetWindowViewport();
viewport->Pos = viewport_min;
viewport->Size = viewport_max - viewport_min;
# if IMGUI_VERSION_NUM > 18002
viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale;
viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale;
# else
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale;
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale;
# endif
}
# endif
m_ViewRect = CalcViewRect(m_View);;
auto& fringeScale = ImFringeScaleRef(m_DrawList);
m_LastFringeScale = fringeScale;
fringeScale *= m_View.InvScale;
}
void ImGuiEx::Canvas::LeaveLocalSpace()
{
IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel);
# if IMGUI_EX_CANVAS_DEFERED()
IM_ASSERT(m_CurrentRange != nullptr);
m_CurrentRange->EndVertexIndex = m_DrawList->_VtxCurrentIdx;
m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size();
if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex)
{
// Drop empty range
m_Ranges.resize(m_Ranges.Size - 1);
}
m_CurrentRange = nullptr;
# endif
// Move vertices to screen space.
auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex;
auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx;
// If canvas view is not scaled take a faster path.
if (m_View.Scale != 1.0f)
{
while (vertex < vertexEnd)
{
vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x;
vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y;
++vertex;
}
// Move clip rectangles to screen space.
for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
{
auto& command = m_DrawList->CmdBuffer[i];
command.ClipRect.x = command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x;
command.ClipRect.y = command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y;
command.ClipRect.z = command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x;
command.ClipRect.w = command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y;
}
}
else
{
while (vertex < vertexEnd)
{
vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x;
vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y;
++vertex;
}
// Move clip rectangles to screen space.
for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); ++i)
{
auto& command = m_DrawList->CmdBuffer[i];
command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x;
command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y;
command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x;
command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y;
}
}
auto& fringeScale = ImFringeScaleRef(m_DrawList);
fringeScale = m_LastFringeScale;
// And pop \o/
ImGui::PopClipRect();
RestoreInputState();
RestoreViewportState();
}
+275
View File
@@ -0,0 +1,275 @@
// Canvas widget - view over infinite virtual space.
//
// Canvas allows you to draw your widgets anywhere over infinite space and provide
// view over it with support for panning and scaling.
//
// When you enter a canvas ImGui is moved to virtual space which mean:
// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top left corner
// of the canvas on the screen (this can be changed usign CanvasView()).
// - Mouse input is brought to canvas space, so widgets works as usual.
// - Everything you draw with ImDrawList will be in virtual space.
//
// By default origin point is on top left corner of canvas widget. It can be
// changed with call to CanvasView() where you can specify what part of space
// should be viewed by setting viewport origin point and scale. Current state
// can be queried with CanvasViewOrigin() and CanvasViewScale().
//
// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can query
// it using CanvasContentMin/Max/Size functions. They are useful if you to not specify
// canvas size in which case all free space is used.
//
// Bounds of visible region of infinite space can be queried using CanvasViewMin/Max/Size
// functions. Everything that is drawn outside of this region will be clipped
// as usual in ImGui.
//
// While drawing inside canvas you can translate position from world (usual ImGui space)
// to virtual space and back usign CanvasFromWorld()/CanvasToWorld().
//
// Canvas can be nested in each other (they are regular widgets after all). There
// is a way to transform position between current and parent canvas with
// CanvasFromParent()/CanvasToParent().
//
// Sometimes in more elaborate scenarios you want to move out canvas virtual space,
// do something and came back. You can do that with SuspendCanvas() and ResumeCanvas().
//
// Note:
// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() scope.
//
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
# ifndef __IMGUI_EX_CANVAS_H__
# define __IMGUI_EX_CANVAS_H__
# pragma once
# include <imgui.h>
# include <imgui_internal.h> // ImRect, ImFloor
namespace ImGuiEx {
struct CanvasView
{
ImVec2 Origin;
float Scale = 1.0f;
float InvScale = 1.0f;
CanvasView() = default;
CanvasView(const ImVec2& origin, float scale)
: Origin(origin)
, Scale(scale)
, InvScale(scale ? 1.0f / scale : 0.0f)
{
}
void Set(const ImVec2& origin, float scale)
{
*this = CanvasView(origin, scale);
}
};
// Canvas widget represent view over infinite plane.
//
// It acts like a child window without scroll bars with
// ability to zoom to specific part of canvas plane.
//
// Widgets are clipped according to current view exactly
// same way ImGui do. To avoid `missing widgets` artifacts first
// setup visible region with SetView() then draw content.
//
// Everything drawn with ImDrawList betwen calls to Begin()/End()
// will be drawn on canvas plane. This behavior can be suspended
// by calling Suspend() and resumed by calling Resume().
//
// Warning:
// Please do not interleave canvas with use of channel splitter.
// Keep channel splitter contained inside canvas or always
// call canvas functions from same channel.
struct Canvas
{
// Begins drawing content of canvas plane.
//
// When false is returned that mean canvas is not visible to the
// user can drawing should be skipped and End() not called.
// When true is returned drawing must be ended with call to End().
//
// If any size component is equal to zero or less canvas will
// automatically expand to all available area on that axis.
// So (0, 300) will take horizontal space and have height
// of 300 points. (0, 0) will take all remaining space of
// the window.
//
// You can query size of the canvas while it is being drawn
// by calling Rect().
bool Begin(const char* id, const ImVec2& size);
bool Begin(ImGuiID id, const ImVec2& size);
// Ends interaction with canvas plane.
//
// Must be called only when Begin() retuned true.
void End();
// Sets visible region of canvas plane.
//
// Origin is an offset of infinite plane origin from top left
// corner of the canvas.
//
// Scale greater than 1 make canvas content be bigger, less than 1 smaller.
void SetView(const ImVec2& origin, float scale);
void SetView(const CanvasView& view);
// Centers view over specific point on canvas plane.
//
// View will be centered on specific point by changing origin
// but not scale.
void CenterView(const ImVec2& canvasPoint);
// Calculates view over specific point on canvas plane.
CanvasView CalcCenterView(const ImVec2& canvasPoint) const;
// Centers view over specific rectangle on canvas plane.
//
// Whole rectangle will fit in canvas view. This will affect both
// origin and scale.
void CenterView(const ImRect& canvasRect);
// Calculates view over specific rectangle on canvas plane.
CanvasView CalcCenterView(const ImRect& canvasRect) const;
// Suspends canvas by returning to normal ImGui transformation space.
// While suspended UI will not be drawn on canvas plane.
//
// Calls to Suspend()/Resume() are symetrical. Each call to Suspend()
// must be matched with call to Resume().
void Suspend();
void Resume();
// Transforms point from canvas plane to ImGui.
ImVec2 FromLocal(const ImVec2& point) const;
ImVec2 FromLocal(const ImVec2& point, const CanvasView& view) const;
// Transforms vector from canvas plant to ImGui.
ImVec2 FromLocalV(const ImVec2& vector) const;
ImVec2 FromLocalV(const ImVec2& vector, const CanvasView& view) const;
// Transforms point from ImGui to canvas plane.
ImVec2 ToLocal(const ImVec2& point) const;
ImVec2 ToLocal(const ImVec2& point, const CanvasView& view) const;
// Transforms vector from ImGui to canvas plane.
ImVec2 ToLocalV(const ImVec2& vector) const;
ImVec2 ToLocalV(const ImVec2& vector, const CanvasView& view) const;
// Returns widget bounds.
//
// Note:
// Rect is valid after call to Begin().
const ImRect& Rect() const { return m_WidgetRect; }
// Returns visible region on canvas plane (in canvas plane coordinates).
const ImRect& ViewRect() const { return m_ViewRect; }
// Calculates visible region for view.
ImRect CalcViewRect(const CanvasView& view) const;
// Returns current view.
const CanvasView& View() const { return m_View; }
// Returns origin of the view.
//
// Origin is an offset of infinite plane origin from top left
// corner of the canvas.
const ImVec2& ViewOrigin() const { return m_View.Origin; }
// Returns scale of the view.
float ViewScale() const { return m_View.Scale; }
// Returns true if canvas is suspended.
//
// See: Suspend()/Resume()
bool IsSuspended() const { return m_SuspendCounter > 0; }
private:
# define IMGUI_EX_CANVAS_DEFERED() 0
# if IMGUI_EX_CANVAS_DEFERED()
struct Range
{
int BeginVertexIndex = 0;
int EndVertexIndex = 0;
int BeginComandIndex = 0;
int EndCommandIndex = 0;
};
# endif
void UpdateViewTransformPosition();
void SaveInputState();
void RestoreInputState();
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
// with a viewport-enabled ImGui (IMGUI_HAS_VIEWPORT, i.e. the docking
// branch), ImGui::IsMouseHoveringRect() — and therefore every item's
// hover test — requires the rect to overlap the mouse viewport's rect,
// which is in *screen* space. Inside the canvas, rects are in canvas
// space, so anything at canvas coordinates outside the screen-space
// range (e.g. negative) went hover/click-dead. Fix: transform the
// viewport rect into canvas-local space while inside EnterLocalSpace()
// and restore it on LeaveLocalSpace(), exactly like the mouse position.
void SaveViewportState();
void RestoreViewportState();
void EnterLocalSpace();
void LeaveLocalSpace();
bool m_InBeginEnd = false;
ImVec2 m_WidgetPosition;
ImVec2 m_WidgetSize;
ImRect m_WidgetRect;
ImDrawList* m_DrawList = nullptr;
int m_ExpectedChannel = 0;
# if IMGUI_EX_CANVAS_DEFERED()
ImVector<Range> m_Ranges;
Range* m_CurrentRange = nullptr;
# endif
int m_DrawListCommadBufferSize = 0;
int m_DrawListStartVertexIndex = 0;
CanvasView m_View;
ImRect m_ViewRect;
ImVec2 m_ViewTransformPosition;
int m_SuspendCounter = 0;
float m_LastFringeScale = 1.0f;
ImVec2 m_MousePosBackup;
ImVec2 m_MousePosPrevBackup;
ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
ImVec2 m_WindowCursorMaxBackup;
# if defined(IMGUI_HAS_VIEWPORT)
ImVec2 m_WindowPosBackup;
ImVec2 m_ViewportPosBackup;
ImVec2 m_ViewportSizeBackup;
# if IMGUI_VERSION_NUM > 18002
ImVec2 m_ViewportWorkPosBackup;
ImVec2 m_ViewportWorkSizeBackup;
# else
ImVec2 m_ViewportWorkOffsetMinBackup;
ImVec2 m_ViewportWorkOffsetMaxBackup;
# endif
# endif
};
} // namespace ImGuiEx
# endif // __IMGUI_EX_CANVAS_H__
@@ -0,0 +1,76 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_EXTRA_MATH_H__
# define __IMGUI_EXTRA_MATH_H__
# pragma once
//------------------------------------------------------------------------------
# include <imgui.h>
# ifndef IMGUI_DEFINE_MATH_OPERATORS
# define IMGUI_DEFINE_MATH_OPERATORS
# endif
# include <imgui_internal.h>
//------------------------------------------------------------------------------
struct ImLine
{
ImVec2 A, B;
};
//------------------------------------------------------------------------------
// Modern ImGui (1.89.4+) already defines these four operators in imgui.h
// itself when IMGUI_DEFINE_MATH_OPERATORS is set before its first include —
// guard against redefinition against this project's ImGui version.
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
inline bool operator==(const ImVec2& lhs, const ImVec2& rhs);
inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs);
inline ImVec2 operator*(const float lhs, const ImVec2& rhs);
inline ImVec2 operator-(const ImVec2& lhs);
# endif
//------------------------------------------------------------------------------
inline float ImLength(float v);
inline float ImLength(const ImVec2& v);
inline float ImLengthSqr(float v);
inline ImVec2 ImNormalized(const ImVec2& v);
//------------------------------------------------------------------------------
inline bool ImRect_IsEmpty(const ImRect& rect);
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge);
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius);
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b);
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b);
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b);
//------------------------------------------------------------------------------
namespace ImEasing {
template <typename V, typename T>
inline V EaseOutQuad(V b, V c, T t)
{
return b - c * (t * (t - 2));
}
} // namespace ImEasing
//------------------------------------------------------------------------------
# include "imgui_extra_math.inl"
//------------------------------------------------------------------------------
# endif // __IMGUI_EXTRA_MATH_H__
@@ -0,0 +1,191 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_EXTRA_MATH_INL__
# define __IMGUI_EXTRA_MATH_INL__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_extra_math.h"
//------------------------------------------------------------------------------
// See guard note in imgui_extra_math.h — already provided by imgui.h on
// this project's ImGui version.
# ifndef IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
inline bool operator==(const ImVec2& lhs, const ImVec2& rhs)
{
return lhs.x == rhs.x && lhs.y == rhs.y;
}
inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs)
{
return lhs.x != rhs.x || lhs.y != rhs.y;
}
inline ImVec2 operator*(const float lhs, const ImVec2& rhs)
{
return ImVec2(lhs * rhs.x, lhs * rhs.y);
}
inline ImVec2 operator-(const ImVec2& lhs)
{
return ImVec2(-lhs.x, -lhs.y);
}
# endif
//------------------------------------------------------------------------------
inline float ImLength(float v)
{
return v;
}
inline float ImLength(const ImVec2& v)
{
return ImSqrt(ImLengthSqr(v));
}
inline float ImLengthSqr(float v)
{
return v * v;
}
inline ImVec2 ImNormalized(const ImVec2& v)
{
return v * ImInvLength(v, 0.0f);
}
//------------------------------------------------------------------------------
inline bool ImRect_IsEmpty(const ImRect& rect)
{
return rect.Min.x >= rect.Max.x
|| rect.Min.y >= rect.Max.y;
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge)
{
if (!snap_to_edge && rect.Contains(p))
return p;
return ImVec2(
(p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x),
(p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y)
);
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius)
{
auto point = ImRect_ClosestPoint(rect, p, snap_to_edge);
const auto offset = p - point;
const auto distance_sq = offset.x * offset.x + offset.y * offset.y;
if (distance_sq <= 0)
return point;
const auto distance = ImSqrt(distance_sq);
return point + offset * (ImMin(distance, radius) * (1.0f / distance));
}
inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other)
{
ImVec2 result;
if (other.Min.x >= rect.Max.x)
result.x = rect.Max.x;
else if (other.Max.x <= rect.Min.x)
result.x = rect.Min.x;
else
result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2;
if (other.Min.y >= rect.Max.y)
result.y = rect.Max.y;
else if (other.Max.y <= rect.Min.y)
result.y = rect.Min.y;
else
result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2;
return result;
}
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b)
{
ImLine result;
result.A = ImRect_ClosestPoint(rect_a, rect_b);
result.B = ImRect_ClosestPoint(rect_b, rect_a);
auto distribute = [](float& a, float& b, float a0, float a1, float b0, float b1)
{
if (a0 >= b1 || a1 <= b0)
return;
const auto aw = a1 - a0;
const auto bw = b1 - b0;
if (aw > bw)
{
b = b0 + bw - bw * (a - a0) / aw;
a = b;
}
else if (aw < bw)
{
a = a0 + aw - aw * (b - b0) / bw;
b = a;
}
};
distribute(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x);
distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y);
return result;
}
inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b)
{
auto line = ImRect_ClosestLine(rect_a, rect_b);
if (radius_a < 0)
radius_a = 0;
if (radius_b < 0)
radius_b = 0;
if (radius_a == 0 && radius_b == 0)
return line;
const auto offset = line.B - line.A;
const auto length_sq = offset.x * offset.x + offset.y * offset.y;
const auto radius_a_sq = radius_a * radius_a;
const auto radius_b_sq = radius_b * radius_b;
if (length_sq <= 0)
return line;
const auto length = ImSqrt(length_sq);
const auto direction = ImVec2(offset.x / length, offset.y / length);
const auto total_radius_sq = radius_a_sq + radius_b_sq;
if (total_radius_sq > length_sq)
{
const auto scale = length / (radius_a + radius_b);
radius_a *= scale;
radius_b *= scale;
}
line.A = line.A + (direction * radius_a);
line.B = line.B - (direction * radius_b);
return line;
}
//------------------------------------------------------------------------------
# endif // __IMGUI_EXTRA_MATH_INL__
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,637 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# include "imgui_node_editor_internal.h"
# include <algorithm>
//------------------------------------------------------------------------------
static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr;
//------------------------------------------------------------------------------
template <typename C, typename I, typename F>
static int BuildIdList(C& container, I* list, int listSize, F&& accept)
{
if (list != nullptr)
{
int count = 0;
for (auto object : container)
{
if (listSize <= 0)
break;
if (accept(object))
{
list[count] = I(object->ID().AsPointer());
++count;
--listSize;
}
}
return count;
}
else
return static_cast<int>(std::count_if(container.begin(), container.end(), accept));
}
//------------------------------------------------------------------------------
ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor(const Config* config)
{
return reinterpret_cast<ax::NodeEditor::EditorContext*>(new ax::NodeEditor::Detail::EditorContext(config));
}
void ax::NodeEditor::DestroyEditor(EditorContext* ctx)
{
if (GetCurrentEditor() == ctx)
SetCurrentEditor(nullptr);
auto editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
delete editor;
}
void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx)
{
s_Editor = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ctx);
}
ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor()
{
return reinterpret_cast<ax::NodeEditor::EditorContext*>(s_Editor);
}
ax::NodeEditor::Style& ax::NodeEditor::GetStyle()
{
return s_Editor->GetStyle();
}
const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex)
{
return s_Editor->GetStyle().GetColorName(colorIndex);
}
void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, const ImVec4& color)
{
s_Editor->GetStyle().PushColor(colorIndex, color);
}
void ax::NodeEditor::PopStyleColor(int count)
{
s_Editor->GetStyle().PopColor(count);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value)
{
s_Editor->GetStyle().PushVar(varIndex, value);
}
void ax::NodeEditor::PopStyleVar(int count)
{
s_Editor->GetStyle().PopVar(count);
}
void ax::NodeEditor::Begin(const char* id, const ImVec2& size)
{
s_Editor->Begin(id, size);
}
void ax::NodeEditor::End()
{
s_Editor->End();
}
void ax::NodeEditor::BeginNode(NodeId id)
{
s_Editor->GetNodeBuilder().Begin(id);
}
void ax::NodeEditor::BeginPin(PinId id, PinKind kind)
{
s_Editor->GetNodeBuilder().BeginPin(id, kind);
}
void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b)
{
s_Editor->GetNodeBuilder().PinRect(a, b);
}
void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b)
{
s_Editor->GetNodeBuilder().PinPivotRect(a, b);
}
void ax::NodeEditor::PinPivotSize(const ImVec2& size)
{
s_Editor->GetNodeBuilder().PinPivotSize(size);
}
void ax::NodeEditor::PinPivotScale(const ImVec2& scale)
{
s_Editor->GetNodeBuilder().PinPivotScale(scale);
}
void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment)
{
s_Editor->GetNodeBuilder().PinPivotAlignment(alignment);
}
void ax::NodeEditor::EndPin()
{
s_Editor->GetNodeBuilder().EndPin();
}
void ax::NodeEditor::Group(const ImVec2& size)
{
s_Editor->GetNodeBuilder().Group(size);
}
void ax::NodeEditor::EndNode()
{
s_Editor->GetNodeBuilder().End();
}
bool ax::NodeEditor::BeginGroupHint(NodeId nodeId)
{
return s_Editor->GetHintBuilder().Begin(nodeId);
}
ImVec2 ax::NodeEditor::GetGroupMin()
{
return s_Editor->GetHintBuilder().GetGroupMin();
}
ImVec2 ax::NodeEditor::GetGroupMax()
{
return s_Editor->GetHintBuilder().GetGroupMax();
}
ImDrawList* ax::NodeEditor::GetHintForegroundDrawList()
{
return s_Editor->GetHintBuilder().GetForegroundDrawList();
}
ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList()
{
return s_Editor->GetHintBuilder().GetBackgroundDrawList();
}
void ax::NodeEditor::EndGroupHint()
{
s_Editor->GetHintBuilder().End();
}
ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node);
else
return nullptr;
}
bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, float thickness/* = 1.0f*/)
{
return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness);
}
void ax::NodeEditor::Flow(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
s_Editor->Flow(link);
}
bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness)
{
auto& context = s_Editor->GetItemCreator();
if (context.Begin())
{
context.SetStyle(ImColor(color), thickness);
return true;
}
else
return false;
}
bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.QueryLink(startId, endId) == Result::True;
}
bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.QueryLink(startId, endId);
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
bool ax::NodeEditor::QueryNewNode(PinId* pinId)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.QueryNode(pinId) == Result::True;
}
bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.QueryNode(pinId);
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
bool ax::NodeEditor::AcceptNewItem()
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
return context.AcceptItem() == Result::True;
}
bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
auto result = context.AcceptItem();
if (result != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
return result == Result::True;
}
void ax::NodeEditor::RejectNewItem()
{
auto& context = s_Editor->GetItemCreator();
context.RejectItem();
}
void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness)
{
using Result = ax::NodeEditor::Detail::CreateItemAction::Result;
auto& context = s_Editor->GetItemCreator();
if (context.RejectItem() != Result::Indeterminate)
context.SetStyle(ImColor(color), thickness);
}
void ax::NodeEditor::EndCreate()
{
auto& context = s_Editor->GetItemCreator();
context.End();
}
bool ax::NodeEditor::BeginDelete()
{
auto& context = s_Editor->GetItemDeleter();
return context.Begin();
}
bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, PinId* endId)
{
auto& context = s_Editor->GetItemDeleter();
return context.QueryLink(linkId, startId, endId);
}
bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId)
{
auto& context = s_Editor->GetItemDeleter();
return context.QueryNode(nodeId);
}
bool ax::NodeEditor::AcceptDeletedItem()
{
auto& context = s_Editor->GetItemDeleter();
return context.AcceptItem();
}
void ax::NodeEditor::RejectDeletedItem()
{
auto& context = s_Editor->GetItemDeleter();
context.RejectItem();
}
void ax::NodeEditor::EndDelete()
{
auto& context = s_Editor->GetItemDeleter();
context.End();
}
void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position)
{
s_Editor->SetNodePosition(nodeId, position);
}
ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId)
{
return s_Editor->GetNodePosition(nodeId);
}
ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId)
{
return s_Editor->GetNodeSize(nodeId);
}
void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
node->CenterOnScreenInNextFrame();
}
void ax::NodeEditor::RestoreNodeState(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
s_Editor->MarkNodeToRestoreState(node);
}
void ax::NodeEditor::Suspend()
{
s_Editor->Suspend();
}
void ax::NodeEditor::Resume()
{
s_Editor->Resume();
}
bool ax::NodeEditor::IsSuspended()
{
return s_Editor->IsSuspended();
}
bool ax::NodeEditor::IsActive()
{
return s_Editor->IsActive();
}
bool ax::NodeEditor::HasSelectionChanged()
{
return s_Editor->HasSelectionChanged();
}
int ax::NodeEditor::GetSelectedObjectCount()
{
return (int)s_Editor->GetSelectedObjects().size();
}
int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
{
return object->AsNode() != nullptr;
});
}
int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
{
return object->AsLink() != nullptr;
});
}
void ax::NodeEditor::ClearSelection()
{
s_Editor->ClearSelection();
}
void ax::NodeEditor::SelectNode(NodeId nodeId, bool append)
{
if (auto node = s_Editor->FindNode(nodeId))
{
if (append)
s_Editor->SelectObject(node);
else
s_Editor->SetSelectedObject(node);
}
}
void ax::NodeEditor::SelectLink(LinkId linkId, bool append)
{
if (auto link = s_Editor->FindLink(linkId))
{
if (append)
s_Editor->SelectObject(link);
else
s_Editor->SetSelectedObject(link);
}
}
void ax::NodeEditor::DeselectNode(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
s_Editor->DeselectObject(node);
}
void ax::NodeEditor::DeselectLink(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
s_Editor->DeselectObject(link);
}
bool ax::NodeEditor::DeleteNode(NodeId nodeId)
{
if (auto node = s_Editor->FindNode(nodeId))
return s_Editor->GetItemDeleter().Add(node);
else
return false;
}
bool ax::NodeEditor::DeleteLink(LinkId linkId)
{
if (auto link = s_Editor->FindLink(linkId))
return s_Editor->GetItemDeleter().Add(link);
else
return false;
}
void ax::NodeEditor::NavigateToContent(float duration)
{
s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration);
}
void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration)
{
s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration);
}
bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId)
{
return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId);
}
bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId)
{
return s_Editor->GetContextMenu().ShowPinContextMenu(pinId);
}
bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId)
{
return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId);
}
bool ax::NodeEditor::ShowBackgroundContextMenu()
{
return s_Editor->GetContextMenu().ShowBackgroundContextMenu();
}
void ax::NodeEditor::EnableShortcuts(bool enable)
{
s_Editor->EnableShortcuts(enable);
}
bool ax::NodeEditor::AreShortcutsEnabled()
{
return s_Editor->AreShortcutsEnabled();
}
bool ax::NodeEditor::BeginShortcut()
{
return s_Editor->GetShortcut().Begin();
}
bool ax::NodeEditor::AcceptCut()
{
return s_Editor->GetShortcut().AcceptCut();
}
bool ax::NodeEditor::AcceptCopy()
{
return s_Editor->GetShortcut().AcceptCopy();
}
bool ax::NodeEditor::AcceptPaste()
{
return s_Editor->GetShortcut().AcceptPaste();
}
bool ax::NodeEditor::AcceptDuplicate()
{
return s_Editor->GetShortcut().AcceptDuplicate();
}
bool ax::NodeEditor::AcceptCreateNode()
{
return s_Editor->GetShortcut().AcceptCreateNode();
}
int ax::NodeEditor::GetActionContextSize()
{
return static_cast<int>(s_Editor->GetShortcut().m_Context.size());
}
int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, [](auto object)
{
return object->AsNode() != nullptr;
});
}
int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size)
{
return BuildIdList(s_Editor->GetSelectedObjects(), links, size, [](auto object)
{
return object->AsLink() != nullptr;
});
}
void ax::NodeEditor::EndShortcut()
{
return s_Editor->GetShortcut().End();
}
float ax::NodeEditor::GetCurrentZoom()
{
return s_Editor->GetView().InvScale;
}
ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode()
{
return s_Editor->GetDoubleClickedNode();
}
ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin()
{
return s_Editor->GetDoubleClickedPin();
}
ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink()
{
return s_Editor->GetDoubleClickedLink();
}
bool ax::NodeEditor::IsBackgroundClicked()
{
return s_Editor->IsBackgroundClicked();
}
bool ax::NodeEditor::IsBackgroundDoubleClicked()
{
return s_Editor->IsBackgroundDoubleClicked();
}
bool ax::NodeEditor::PinHadAnyLinks(PinId pinId)
{
return s_Editor->PinHadAnyLinks(pinId);
}
ImVec2 ax::NodeEditor::GetScreenSize()
{
return s_Editor->GetRect().GetSize();
}
ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos)
{
return s_Editor->ToCanvas(pos);
}
ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos)
{
return s_Editor->ToScreen(pos);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// LICENSE
// This software is dual-licensed to the public domain and under the following
// license: you are granted a perpetual, irrevocable license to copy, modify,
// publish, and distribute this file as you see fit.
//
// CREDITS
// Written by Michal Cichon
//------------------------------------------------------------------------------
# ifndef __IMGUI_NODE_EDITOR_INTERNAL_INL__
# define __IMGUI_NODE_EDITOR_INTERNAL_INL__
# pragma once
//------------------------------------------------------------------------------
# include "imgui_node_editor_internal.h"
//------------------------------------------------------------------------------
namespace ax {
namespace NodeEditor {
namespace Detail {
//------------------------------------------------------------------------------
//inline ImRect ToRect(const ax::rectf& rect)
//{
// return ImRect(
// to_imvec(rect.top_left()),
// to_imvec(rect.bottom_right())
// );
//}
//
//inline ImRect ToRect(const ax::rect& rect)
//{
// return ImRect(
// to_imvec(rect.top_left()),
// to_imvec(rect.bottom_right())
// );
//}
inline ImRect ImGui_GetItemRect()
{
return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
}
//------------------------------------------------------------------------------
} // namespace Detail
} // namespace Editor
} // namespace ax
//------------------------------------------------------------------------------
# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__
+381
View File
@@ -0,0 +1,381 @@
#include "NodeThumbnailCache.h"
#include "../utils/GLExt.h"
#include <pxr/imaging/hio/image.h>
#include <pxr/imaging/hio/types.h>
#include <pxr/base/gf/half.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace UsdLayerManager {
namespace {
// Mix a graph revision with a node path so switching which node the shared
// preview renderer draws always reads as "changed" to MaterialPreviewRenderer's
// revision gate (it treats the value as an opaque change token), while a real
// graph edit (revision bump) still forces a re-render.
size_t RenderToken(size_t revision, const std::string& nodeKey) {
size_t h = revision * 1099511628211ull;
h ^= std::hash<std::string>{}(nodeKey) + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2);
return h;
}
} // namespace
NodeThumbnailCache::NodeThumbnailCache() {
// Prime Hio's plugin registry on the main thread so the worker never
// first-touches plugin discovery off-thread.
pxr::HioImage::IsSupportedImageFile("prime.png");
m_running = true;
m_worker = std::thread(&NodeThumbnailCache::WorkerLoop, this);
}
NodeThumbnailCache::~NodeThumbnailCache() {
m_running = false;
m_jobCv.notify_all();
if (m_worker.joinable())
m_worker.join();
Clear();
if (m_readFbo) glDeleteFramebuffers(1, &m_readFbo);
if (m_drawFbo) glDeleteFramebuffers(1, &m_drawFbo);
}
// ---------------------------------------------------------------------------
// Texture thumbnails
// ---------------------------------------------------------------------------
ImTextureID NodeThumbnailCache::GetTextureThumbnail(const pxr::SdfPath& node,
const std::string& resolvedFile) {
const std::string key = node.GetString();
TexEntry& e = m_texEntries[key];
e.touched = true;
if (resolvedFile.empty())
return e.texId;
// (Re)request when the file differs from both the uploaded and in-flight one.
if (resolvedFile != e.fileKey && resolvedFile != e.reqFile) {
e.reqFile = resolvedFile;
{
std::lock_guard<std::mutex> lk(m_jobMutex);
m_jobs.push_back(TexJob{key, resolvedFile});
}
m_jobCv.notify_one();
}
return e.texId;
}
void NodeThumbnailCache::WorkerLoop() {
for (;;) {
TexJob job;
{
std::unique_lock<std::mutex> lk(m_jobMutex);
m_jobCv.wait(lk, [this] { return !m_running || !m_jobs.empty(); });
if (!m_running) return;
job = std::move(m_jobs.front());
m_jobs.pop_front();
}
TexResult result;
result.nodeKey = job.nodeKey;
result.file = job.file;
result.ok = DecodeThumbnail(job.file, result);
{
std::lock_guard<std::mutex> lk(m_resultMutex);
m_results.push_back(std::move(result));
}
}
}
bool NodeThumbnailCache::DecodeThumbnail(const std::string& file, TexResult& out) {
pxr::HioImageSharedPtr img = pxr::HioImage::OpenForReading(file, /*subimage=*/0, /*mip=*/0,
pxr::HioImage::SourceColorSpace::Auto,
/*suppressErrors=*/true);
if (!img) return false;
const int sw = img->GetWidth();
const int sh = img->GetHeight();
if (sw <= 0 || sh <= 0) return false;
// Hio's stb reader does no format conversion: it requires the requested
// StorageSpec.format to *exactly* equal the file's own format (channel
// count + component type + sRGB-ness) or it raises "Image format mismatch".
// So read into the image's native format and normalize to RGBA8 ourselves.
const pxr::HioFormat fmt = img->GetFormat();
const int nComp = pxr::HioGetComponentCount(fmt);
const pxr::HioType type = pxr::HioGetHioType(fmt);
if (nComp < 1 || nComp > 4) return false;
size_t bpc = 0; // bytes per component
switch (type) {
case pxr::HioTypeUnsignedByte:
case pxr::HioTypeUnsignedByteSRGB:
case pxr::HioTypeSignedByte: bpc = 1; break;
case pxr::HioTypeUnsignedShort:
case pxr::HioTypeSignedShort:
case pxr::HioTypeHalfFloat: bpc = 2; break;
case pxr::HioTypeUnsignedInt:
case pxr::HioTypeInt:
case pxr::HioTypeFloat: bpc = 4; break;
case pxr::HioTypeDouble: bpc = 8; break;
default: return false;
}
const size_t pixels = static_cast<size_t>(sw) * sh;
std::vector<unsigned char> raw(pixels * nComp * bpc);
pxr::HioImage::StorageSpec spec;
spec.width = sw;
spec.height = sh;
spec.depth = 1;
spec.format = fmt;
spec.flipped = false; // keep top row first for ImGui display
spec.data = raw.data();
if (!img->Read(spec)) return false;
// Collapse whatever component type the file uses down to an 8-bit value.
// sRGB byte data is passed through as-is (ImGui does no color management,
// so the encoded bytes display correctly); float/half is clamped to [0,1].
const void* rp = raw.data();
auto toU8 = [&](size_t i) -> unsigned char {
switch (type) {
case pxr::HioTypeUnsignedByte:
case pxr::HioTypeUnsignedByteSRGB:
return reinterpret_cast<const uint8_t*>(rp)[i];
case pxr::HioTypeSignedByte:
return static_cast<unsigned char>(std::max(0, static_cast<int>(reinterpret_cast<const int8_t*>(rp)[i]) * 2));
case pxr::HioTypeUnsignedShort:
return static_cast<unsigned char>(reinterpret_cast<const uint16_t*>(rp)[i] >> 8);
case pxr::HioTypeSignedShort:
return static_cast<unsigned char>(std::clamp(static_cast<int>(reinterpret_cast<const int16_t*>(rp)[i]) >> 7, 0, 255));
case pxr::HioTypeHalfFloat:
return static_cast<unsigned char>(std::clamp(static_cast<float>(reinterpret_cast<const pxr::GfHalf*>(rp)[i]), 0.f, 1.f) * 255.f + 0.5f);
case pxr::HioTypeFloat:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const float*>(rp)[i], 0.f, 1.f) * 255.f + 0.5f);
case pxr::HioTypeUnsignedInt:
return static_cast<unsigned char>(reinterpret_cast<const uint32_t*>(rp)[i] >> 24);
case pxr::HioTypeInt:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const int32_t*>(rp)[i] >> 23, 0, 255));
case pxr::HioTypeDouble:
return static_cast<unsigned char>(std::clamp(reinterpret_cast<const double*>(rp)[i], 0.0, 1.0) * 255.0 + 0.5);
default: return 0;
}
};
// Expand to RGBA8: 1ch → grey, 2ch → grey+alpha, 3ch → opaque, 4ch → as-is.
std::vector<unsigned char> native(pixels * 4);
for (size_t px = 0; px < pixels; ++px) {
const size_t s = px * nComp;
const unsigned char c0 = toU8(s);
unsigned char* d = &native[px * 4];
d[0] = c0;
d[1] = nComp >= 3 ? toU8(s + 1) : c0;
d[2] = nComp >= 3 ? toU8(s + 2) : c0;
d[3] = nComp == 4 ? toU8(s + 3) : (nComp == 2 ? toU8(s + 1) : 255);
}
const int longSide = std::max(sw, sh);
const float scale = longSide > kThumbPx ? static_cast<float>(kThumbPx) / longSide : 1.0f;
const int dw = std::max(1, static_cast<int>(std::lround(sw * scale)));
const int dh = std::max(1, static_cast<int>(std::lround(sh * scale)));
if (dw == sw && dh == sh) {
out.rgba = std::move(native);
} else {
out.rgba.assign(static_cast<size_t>(dw) * dh * 4, 0);
for (int y = 0; y < dh; ++y) {
const int sy0 = y * sh / dh, sy1 = std::max(sy0 + 1, (y + 1) * sh / dh);
for (int x = 0; x < dw; ++x) {
const int sx0 = x * sw / dw, sx1 = std::max(sx0 + 1, (x + 1) * sw / dw);
unsigned int acc[4] = {0, 0, 0, 0};
unsigned int n = 0;
for (int sy = sy0; sy < sy1; ++sy)
for (int sx = sx0; sx < sx1; ++sx) {
const unsigned char* p = &native[(static_cast<size_t>(sy) * sw + sx) * 4];
acc[0] += p[0]; acc[1] += p[1]; acc[2] += p[2]; acc[3] += p[3];
++n;
}
unsigned char* d = &out.rgba[(static_cast<size_t>(y) * dw + x) * 4];
d[0] = static_cast<unsigned char>(acc[0] / n);
d[1] = static_cast<unsigned char>(acc[1] / n);
d[2] = static_cast<unsigned char>(acc[2] / n);
d[3] = static_cast<unsigned char>(acc[3] / n);
}
}
}
out.w = dw;
out.h = dh;
return true;
}
void NodeThumbnailCache::UploadTexture(TexEntry& e, const TexResult& r) {
DeleteTex(e.texId);
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, r.w, r.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, r.rgba.data());
glBindTexture(GL_TEXTURE_2D, 0);
e.texId = static_cast<ImTextureID>(static_cast<ImU64>(tex));
}
// ---------------------------------------------------------------------------
// Material thumbnails
// ---------------------------------------------------------------------------
ImTextureID NodeThumbnailCache::GetMaterialThumbnail(const pxr::UsdStageRefPtr& stage,
const pxr::SdfPath& materialPath,
const pxr::SdfPath& node,
const std::string& output, bool terminal,
size_t revision) {
MatEntry& e = m_matEntries[node.GetString()];
e.touched = true;
e.stage = stage;
e.materialPath = materialPath;
e.nodePath = node;
e.output = output;
e.terminal = terminal;
if (revision != e.revision || e.texId == 0) {
e.revision = revision;
e.stale = true;
}
return e.texId;
}
void NodeThumbnailCache::SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
const std::string& ocioDisplay, const std::string& ocioView,
const std::string& ocioColorSpace, const std::string& ocioLook) {
const bool changed = !m_renderSettingsInitialized ||
rendererPlugin != m_lastRendererPlugin || ccMode != m_lastCcMode ||
ocioDisplay != m_lastOcioDisplay || ocioView != m_lastOcioView ||
ocioColorSpace != m_lastOcioColorSpace || ocioLook != m_lastOcioLook;
if (!changed) return;
m_renderSettingsInitialized = true;
m_lastRendererPlugin = rendererPlugin;
m_lastCcMode = ccMode;
m_lastOcioDisplay = ocioDisplay;
m_lastOcioView = ocioView;
m_lastOcioColorSpace = ocioColorSpace;
m_lastOcioLook = ocioLook;
if (!rendererPlugin.IsEmpty())
m_matRenderer.SetRendererPlugin(rendererPlugin);
m_matRenderer.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
// Every cached shader-ball thumbnail was rendered under the old settings.
for (auto& kv : m_matEntries)
kv.second.stale = true;
}
void NodeThumbnailCache::EnsureBlitFbos() {
if (!m_readFbo) glGenFramebuffers(1, &m_readFbo);
if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo);
}
void NodeThumbnailCache::BlitToEntry(MatEntry& e, uint32_t srcTex) {
int sw = 0, sh = 0;
glBindTexture(GL_TEXTURE_2D, srcTex);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &sw);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &sh);
glBindTexture(GL_TEXTURE_2D, 0);
if (sw <= 0 || sh <= 0) return;
if (e.texId == 0) {
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kThumbPx, kThumbPx, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
e.texId = static_cast<ImTextureID>(static_cast<ImU64>(tex));
}
const GLuint dst = static_cast<GLuint>(static_cast<ImU64>(e.texId));
EnsureBlitFbos();
GLint prevRead = 0, prevDraw = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDraw);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_readFbo);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTex, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_drawFbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0);
// Flip Y (dst rows reversed): the draw target is a GL bottom-up texture, so
// this leaves the cache texture top-down for uniform ImGui (0,0)-(1,1) UVs.
glBlitFramebuffer(0, 0, sw, sh, 0, kThumbPx, kThumbPx, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevRead));
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(prevDraw));
}
// ---------------------------------------------------------------------------
// Per-frame pump / lifecycle
// ---------------------------------------------------------------------------
void NodeThumbnailCache::PumpMainThread() {
// 1. Upload finished texture decodes.
std::vector<TexResult> done;
{
std::lock_guard<std::mutex> lk(m_resultMutex);
done.swap(m_results);
}
for (const TexResult& r : done) {
auto it = m_texEntries.find(r.nodeKey);
if (it == m_texEntries.end()) continue;
TexEntry& e = it->second;
if (r.file != e.reqFile) continue; // superseded by a newer request
if (r.ok) {
UploadTexture(e, r);
e.fileKey = r.file;
}
e.reqFile.clear();
}
// 2. Render at most one stale material thumbnail (amortized).
for (auto& kv : m_matEntries) {
MatEntry& e = kv.second;
if (!e.stale || !e.touched || !e.stage) continue;
m_matRenderer.SetMaterial(e.stage, e.materialPath, RenderToken(e.revision, kv.first),
e.nodePath, e.output, e.terminal);
const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx);
if (src != 0) {
BlitToEntry(e, src);
// Progressive delegates (Arnold/Cycles/Embree) need several
// Render() calls to accumulate past their first noisy sample;
// keep re-pumping this entry until the image is final instead of
// caching whatever the first pass produced.
e.stale = !m_matRenderer.IsConverged();
}
break; // one per frame
}
// 3. Prune entries not requested since the previous pump (node deleted or
// no longer a thumbnailed category), then reset touch flags for this frame.
for (auto it = m_texEntries.begin(); it != m_texEntries.end();) {
if (!it->second.touched) { DeleteTex(it->second.texId); it = m_texEntries.erase(it); }
else { it->second.touched = false; ++it; }
}
for (auto it = m_matEntries.begin(); it != m_matEntries.end();) {
if (!it->second.touched) { DeleteTex(it->second.texId); it = m_matEntries.erase(it); }
else { it->second.touched = false; ++it; }
}
}
void NodeThumbnailCache::Clear() {
for (auto& kv : m_texEntries) DeleteTex(kv.second.texId);
for (auto& kv : m_matEntries) DeleteTex(kv.second.texId);
m_texEntries.clear();
m_matEntries.clear();
}
void NodeThumbnailCache::DeleteTex(ImTextureID& id) {
if (id) {
GLuint t = static_cast<GLuint>(static_cast<ImU64>(id));
glDeleteTextures(1, &t);
id = 0;
}
}
} // namespace UsdLayerManager
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include "MaterialPreviewRenderer.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/tf/token.h>
#include <imgui.h>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
namespace UsdLayerManager {
/// Owns the in-canvas node thumbnails for the material editor.
///
/// Two kinds, both handed back as an ImTextureID (0 while not ready):
/// - Texture nodes: the source image, decoded on a background worker thread
/// (USD HioImage / OpenImageIO) and uploaded to GL on the main thread.
/// - Material nodes: a shader-ball render through a dedicated
/// MaterialPreviewRenderer. Hydra is GL-/main-thread-bound, so these are
/// rendered one-per-frame (amortized) and cached by graph revision.
///
/// Lifecycle is driven from the panel: Get*Thumbnail() per node during the
/// canvas draw, then PumpMainThread() once per frame (GL context current).
class NodeThumbnailCache {
public:
NodeThumbnailCache();
~NodeThumbnailCache();
NodeThumbnailCache(const NodeThumbnailCache&) = delete;
NodeThumbnailCache& operator=(const NodeThumbnailCache&) = delete;
/// Displayed edge length; also the material render size.
static constexpr int kThumbPx = 96;
/// Returns the node's texture thumbnail (0 = not ready). Enqueues a
/// background decode when resolvedFile is new or has changed. An empty
/// resolvedFile (unresolvable asset) yields whatever is cached (usually 0).
ImTextureID GetTextureThumbnail(const pxr::SdfPath& node, const std::string& resolvedFile);
/// Returns the node's material shader-ball thumbnail (0 = not ready yet).
/// Marks it stale (to be re-rendered on a later pump) when revision changes.
/// output/terminal select the node output to preview (a token-typed
/// terminal is wired as the surface; otherwise routed into diffuseColor).
ImTextureID GetMaterialThumbnail(const pxr::UsdStageRefPtr& stage,
const pxr::SdfPath& materialPath,
const pxr::SdfPath& node,
const std::string& output, bool terminal,
size_t revision);
/// Keeps the material shader-ball thumbnails' render delegate and color
/// correction in sync with the main preview swatch's current settings
/// (they'd otherwise render under this cache's own independent Hydra
/// instance's defaults, mismatching the preview and the app's global
/// color-correction). Cheap to call every frame: no-ops unless something
/// actually changed, and only then invalidates cached thumbnails so
/// PumpMainThread re-renders them under the new settings. An empty
/// rendererPlugin means "not resolved yet" and is left alone.
void SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
const std::string& ocioDisplay, const std::string& ocioView,
const std::string& ocioColorSpace, const std::string& ocioLook);
/// Main thread, once per frame, GL context current: drains finished
/// decodes and uploads them, renders at most one stale material thumbnail,
/// and prunes entries not requested since the previous pump.
void PumpMainThread();
/// Frees every thumbnail (call on material/stage change). Main thread.
void Clear();
private:
// ---- texture thumbnails (background decode) ----
struct TexJob { std::string nodeKey; std::string file; };
struct TexResult { std::string nodeKey; std::string file;
std::vector<unsigned char> rgba; int w = 0, h = 0; bool ok = false; };
struct TexEntry { ImTextureID texId = 0; std::string fileKey; std::string reqFile;
bool touched = false; };
void WorkerLoop();
static bool DecodeThumbnail(const std::string& file, TexResult& out);
void UploadTexture(TexEntry& e, const TexResult& r);
std::thread m_worker;
std::atomic<bool> m_running{false};
std::mutex m_jobMutex;
std::condition_variable m_jobCv;
std::deque<TexJob> m_jobs;
std::mutex m_resultMutex;
std::vector<TexResult> m_results;
std::unordered_map<std::string, TexEntry> m_texEntries;
// ---- material thumbnails (main-thread, amortized) ----
struct MatEntry {
ImTextureID texId = 0;
size_t revision = 0;
bool stale = true;
bool touched = false;
pxr::UsdStageRefPtr stage;
pxr::SdfPath materialPath;
pxr::SdfPath nodePath;
std::string output;
bool terminal = false;
};
std::unordered_map<std::string, MatEntry> m_matEntries;
MaterialPreviewRenderer m_matRenderer;
unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit
unsigned int m_drawFbo = 0;
// Last render settings applied via SyncRenderSettings, so repeated
// per-frame calls with unchanged values are cheap no-ops.
bool m_renderSettingsInitialized = false;
pxr::TfToken m_lastRendererPlugin;
int m_lastCcMode = -1;
std::string m_lastOcioDisplay, m_lastOcioView, m_lastOcioColorSpace, m_lastOcioLook;
void EnsureBlitFbos();
void BlitToEntry(MatEntry& e, uint32_t srcTex);
static void DeleteTex(ImTextureID& id);
};
} // namespace UsdLayerManager
+765 -53
View File
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -3,6 +3,7 @@
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/commands/TransformCommand.h" #include "../core/commands/TransformCommand.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/timeCode.h> #include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
@@ -10,6 +11,7 @@
#include <pxr/base/gf/vec3d.h> #include <pxr/base/gf/vec3d.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h> #include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <imgui.h> #include <imgui.h>
#include <functional>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -24,6 +26,7 @@ public:
void SetPropertyManager(PropertyManager* manager); void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
void SetStage(pxr::UsdStageRefPtr stage); void SetStage(pxr::UsdStageRefPtr stage);
void SetSelectedPrimPath(const std::string& path); void SetSelectedPrimPath(const std::string& path);
@@ -33,6 +36,11 @@ public:
void Render(); void Render();
/// Fired when the user clicks "Edit Material" next to the resolved
/// binding in the Material Binding section, with the resolved material's
/// path. Wired by Application to open the Material Editor on that material.
std::function<void(const std::string&)> OnEditMaterialRequested;
private: private:
void ReadTransform(); void ReadTransform();
void WriteTranslate(); void WriteTranslate();
@@ -52,10 +60,22 @@ private:
void RenderPrimHeader(const pxr::UsdPrim& prim); ///< fixed-height header child void RenderPrimHeader(const pxr::UsdPrim& prim); ///< fixed-height header child
void RenderVariantSetsSection(const pxr::UsdPrim& prim); void RenderVariantSetsSection(const pxr::UsdPrim& prim);
void RenderTransformSection(); void RenderTransformSection();
void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< unified attr+rel table void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema
void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info
/// References/payloads authored on the prim, one row per list-op entry
/// (explicit/add/prepend/append/delete) with its asset path; clicking a
/// path browses for a replacement file (undoable).
void RenderCompositionArcsSection(const pxr::UsdPrim& prim);
/// Read-only view of the prim's composed assetInfo dictionary
/// (identifier, name, version, ...).
void RenderAssetInfoSection(const pxr::UsdPrim& prim);
void RenderAttrRelSubTable(const std::vector<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& rels,
const pxr::UsdEditTarget& editTarget); ///< inner 3-col table
PropertyManager* m_propertyManager = nullptr; PropertyManager* m_propertyManager = nullptr;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr;
pxr::UsdStageRefPtr m_stage; pxr::UsdStageRefPtr m_stage;
std::string m_selectedPrimPath; std::string m_selectedPrimPath;
+263
View File
@@ -0,0 +1,263 @@
#include "RenderLayerPanel.h"
#include "../core/commands/RenderLayerCommands.h"
#include "../core/commands/RenderLayerMembershipCommand.h"
#include <pxr/usd/usd/prim.h>
#include <imgui.h>
#include <cstring>
namespace UsdLayerManager {
RenderLayerPanel::RenderLayerPanel() {}
RenderLayerPanel::~RenderLayerPanel() {}
void RenderLayerPanel::SetStage(pxr::UsdStageRefPtr stage) {
m_stage = stage;
m_defaultSelected = true;
m_selectedLayerId.clear();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::Render() {
if (!m_renderLayerManager) {
ImGui::TextDisabled("No stage loaded");
return;
}
ImGui::Text("Render Layers");
ImGui::Separator();
ImGui::SetNextItemWidth(180.0f);
ImGui::InputText("##newLayerName", m_newLayerNameBuf, sizeof(m_newLayerNameBuf));
ImGui::SameLine();
if (ImGui::Button("Create Layer")) {
std::string name = m_newLayerNameBuf[0] ? m_newLayerNameBuf : "RenderLayer";
std::string newId;
if (m_commandHistory) {
auto cmd = std::make_unique<RenderLayerCreateCommand>(m_renderLayerManager, name);
RenderLayerCreateCommand* raw = cmd.get();
m_commandHistory->Push(std::move(cmd));
newId = raw->GetCreatedLayerId();
} else {
newId = m_renderLayerManager->CreateRenderLayer(name);
}
if (!newId.empty()) {
m_defaultSelected = false;
m_selectedLayerId = newId;
}
}
ImGui::Spacing();
RenderLayerList();
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Membership");
ImGui::Separator();
RenderMembershipPanel();
RenderRenameModal();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderLayerList() {
auto layers = m_renderLayerManager->GetRenderLayers();
if (!ImGui::BeginTable("RenderLayerTable", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
ImVec2(0, 150)))
return;
ImGui::TableSetupColumn("##active", ImGuiTableColumnFlags_WidthFixed, 24.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
const auto& li = layers[i];
ImGui::TableNextRow();
ImGui::PushID(i);
// Col 0: activate radio button.
ImGui::TableSetColumnIndex(0);
{
bool isActive = li.isActive;
if (ImGui::RadioButton("##act", isActive) && !isActive) {
std::string target = li.isDefault ? std::string() : li.layerIdentifier;
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<RenderLayerActivateCommand>(m_renderLayerManager, target));
else if (li.isDefault)
m_renderLayerManager->ActivateDefaultLayer();
else
m_renderLayerManager->ActivateRenderLayer(target);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(isActive ? "Active" : "Click to activate");
}
// Col 1: name — selecting a row picks which layer's membership shows below.
ImGui::TableSetColumnIndex(1);
{
bool selected = li.isDefault
? m_defaultSelected
: (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier);
ImVec4 nameColor = li.isActive ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f)
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
if (ImGui::Selectable(li.name.c_str(), selected,
ImGuiSelectableFlags_SpanAllColumns)) {
m_defaultSelected = li.isDefault;
m_selectedLayerId = li.isDefault ? std::string() : li.layerIdentifier;
}
ImGui::PopStyleColor();
if (!li.isDefault && ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Rename")) {
m_openRenameModal = true;
m_renameTargetId = li.layerIdentifier;
std::strncpy(m_renameBuf, li.name.c_str(), sizeof(m_renameBuf) - 1);
m_renameBuf[sizeof(m_renameBuf) - 1] = '\0';
}
if (ImGui::MenuItem("Delete")) {
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<RenderLayerDeleteCommand>(m_renderLayerManager, li.layerIdentifier));
else
m_renderLayerManager->DeleteRenderLayer(li.layerIdentifier);
if (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier) {
m_defaultSelected = true;
m_selectedLayerId.clear();
}
}
ImGui::EndPopup();
}
}
// Col 2: dirty indicator.
ImGui::TableSetColumnIndex(2);
if (!li.isDefault && li.layer && li.layer->IsDirty())
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
else
ImGui::TextDisabled(" ");
ImGui::PopID();
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderMembershipPanel() {
if (m_defaultSelected) {
ImGui::TextDisabled("Default has no membership of its own - it always shows the full, unmodified stage.");
return;
}
if (m_selectedLayerId.empty()) {
ImGui::TextDisabled("Select a render layer above to edit its membership.");
return;
}
std::string displayName = m_selectedLayerId;
for (const auto& li : m_renderLayerManager->GetRenderLayers())
if (!li.isDefault && li.layerIdentifier == m_selectedLayerId) { displayName = li.name; break; }
ImGui::Text("Members of \"%s\"", displayName.c_str());
auto members = m_renderLayerManager->GetMembers(m_selectedLayerId);
auto isValid = [&](const pxr::SdfPath& p) {
return m_stage && m_stage->GetPrimAtPath(p).IsValid();
};
bool anyMissing = false;
for (const auto& p : members)
if (!isValid(p)) { anyMissing = true; break; }
if (m_sceneHierarchyPanel && ImGui::Button("Add Selected")) {
auto paths = m_sceneHierarchyPanel->GetSelectedPaths();
if (!paths.empty()) {
std::vector<pxr::SdfPath> sdfPaths;
sdfPaths.reserve(paths.size());
for (const auto& p : paths) sdfPaths.emplace_back(p);
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerAddMembersCommand>(
m_renderLayerManager, m_selectedLayerId, sdfPaths));
else
for (const auto& sp : sdfPaths) m_renderLayerManager->AddMember(m_selectedLayerId, sp);
}
}
if (anyMissing) {
ImGui::SameLine();
if (ImGui::Button("Remove Missing")) {
std::vector<pxr::SdfPath> toRemove;
for (const auto& p : members)
if (!isValid(p)) toRemove.push_back(p);
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRemoveMembersCommand>(
m_renderLayerManager, m_selectedLayerId, toRemove));
else
for (const auto& p : toRemove) m_renderLayerManager->RemoveMember(m_selectedLayerId, p);
}
}
if (members.empty()) {
ImGui::TextDisabled("No members. Select lights/objects in Scene Hierarchy and click \"Add Selected\".");
return;
}
if (ImGui::BeginChild("RenderLayerMembersList", ImVec2(0, 0), true)) {
for (const auto& path : members) {
ImGui::PushID(path.GetString().c_str());
bool missing = !isValid(path);
if (missing) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.45f, 1.0f));
ImGui::TextUnformatted(path.GetText());
if (missing) {
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("(missing)");
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - 24.0f);
if (ImGui::SmallButton("x")) {
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRemoveMembersCommand>(
m_renderLayerManager, m_selectedLayerId, std::vector<pxr::SdfPath>{path}));
else
m_renderLayerManager->RemoveMember(m_selectedLayerId, path);
}
ImGui::PopID();
}
}
ImGui::EndChild();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderRenameModal() {
if (m_openRenameModal) {
ImGui::OpenPopup("Rename Render Layer");
m_openRenameModal = false;
}
if (ImGui::BeginPopupModal("Rename Render Layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SetNextItemWidth(240.0f);
bool enterPressed = ImGui::InputText("Name", m_renameBuf, sizeof(m_renameBuf),
ImGuiInputTextFlags_EnterReturnsTrue);
bool okClicked = ImGui::Button("OK");
ImGui::SameLine();
bool cancelClicked = ImGui::Button("Cancel");
if ((enterPressed || okClicked) && m_renameBuf[0]) {
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRenameCommand>(
m_renderLayerManager, m_renameTargetId, std::string(m_renameBuf)));
else
m_renderLayerManager->RenameRenderLayer(m_renameTargetId, m_renameBuf);
ImGui::CloseCurrentPopup();
} else if (cancelClicked) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
} // namespace UsdLayerManager
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "../core/RenderLayerManager.h"
#include "../core/CommandHistory.h"
#include "SceneHierarchyPanel.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <string>
namespace UsdLayerManager {
/// Maya-Render-Layers-style panel: layer list with one-click activation,
/// create/rename/delete, and a membership editor for whichever layer is
/// selected (independent of which one is active — you can inspect/edit a
/// layer's members without switching to it).
class RenderLayerPanel {
public:
RenderLayerPanel();
~RenderLayerPanel();
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
/// Optional — powers the "Add Selected" membership button.
void SetSceneHierarchyPanel(SceneHierarchyPanel* panel) { m_sceneHierarchyPanel = panel; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
void SetStage(pxr::UsdStageRefPtr stage);
void Render();
private:
void RenderLayerList();
void RenderMembershipPanel();
void RenderRenameModal();
RenderLayerManager* m_renderLayerManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
SceneHierarchyPanel* m_sceneHierarchyPanel = nullptr;
IconManager* m_iconManager = nullptr;
pxr::UsdStageRefPtr m_stage;
/// Which layer's membership the lower panel shows — independent of which
/// layer is currently active.
bool m_defaultSelected = true;
std::string m_selectedLayerId;
char m_newLayerNameBuf[128] = "RenderLayer";
bool m_openRenameModal = false;
std::string m_renameTargetId;
char m_renameBuf[128] = {};
};
} // namespace UsdLayerManager
File diff suppressed because it is too large Load Diff
+47 -1
View File
@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h" #include "IconManager.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
@@ -26,6 +27,11 @@ public:
void SetPropertyManager(PropertyManager* manager); void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetLayerManager(LayerManager* lm) { m_layerManager = lm; } void SetLayerManager(LayerManager* lm) { m_layerManager = lm; }
/// Optional. When set and a non-Default render layer is active, the
/// edit-target layer switcher below is disabled — repointing the ambient
/// edit target away from the active render layer's SdfLayer would break
/// the invariant that edits made while it's active land in that layer.
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void SetStage(UsdStageRefPtr stage); void SetStage(UsdStageRefPtr stage);
void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; } void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
void Render(); void Render();
@@ -33,6 +39,12 @@ public:
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; } std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
UsdPrim GetSelectedPrim() const; UsdPrim GetSelectedPrim() const;
/// Full multi-selection (rect pick or Ctrl/Shift click), in no
/// particular order. Empty when nothing is selected.
std::vector<std::string> GetSelectedPaths() const {
return std::vector<std::string>(m_selectedPaths.begin(), m_selectedPaths.end());
}
/// Set single selection from hierarchy click (fires callback). /// Set single selection from hierarchy click (fires callback).
/// Also clears any rect multi-selection. /// Also clears any rect multi-selection.
void SetSelectedPathFromClick(const std::string& path); void SetSelectedPathFromClick(const std::string& path);
@@ -62,21 +74,30 @@ public:
using PrimSelectCallback = std::function<void(const std::string& path)>; using PrimSelectCallback = std::function<void(const std::string& path)>;
void SetOnPrimSelected(PrimSelectCallback callback) { m_onPrimSelected = callback; } void SetOnPrimSelected(PrimSelectCallback callback) { m_onPrimSelected = callback; }
using MultiPrimSelectCallback = std::function<void(const std::vector<std::string>&)>;
void SetOnPrimsSelected(MultiPrimSelectCallback cb) { m_onPrimsSelected = cb; }
/// Called when stage-level metadata (e.g. up axis) is changed via the hierarchy panel. /// Called when stage-level metadata (e.g. up axis) is changed via the hierarchy panel.
using StageMetadataChangedCallback = std::function<void()>; using StageMetadataChangedCallback = std::function<void()>;
void SetOnStageMetadataChanged(StageMetadataChangedCallback callback) { m_onStageMetadataChanged = callback; } void SetOnStageMetadataChanged(StageMetadataChangedCallback callback) { m_onStageMetadataChanged = callback; }
enum class ArcModalMode { None, Inherit, Specialize, VariantSet, Variant };
private: private:
void RenderPrimNode(const UsdPrim& prim); void RenderPrimNode(const UsdPrim& prim);
const char* GetPrimTypeIcon(const UsdPrim& prim) const; const char* GetPrimTypeIcon(const UsdPrim& prim) const;
Icon GetPrimTypeIconEnum(const UsdPrim& prim) const; Icon GetPrimTypeIconEnum(const UsdPrim& prim) const;
void RenderContextMenu(const UsdPrim& prim); void RenderContextMenu(const UsdPrim& prim);
void RenderRemovePrimModal(); void RenderRemovePrimModal();
void RenderArcModal();
void ProcessPendingReplaceRef(); void ProcessPendingReplaceRef();
void RenderSublayerSection();
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers);
PropertyManager* m_propertyManager; PropertyManager* m_propertyManager;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
LayerManager* m_layerManager = nullptr; LayerManager* m_layerManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
IconManager* m_iconManager = nullptr; IconManager* m_iconManager = nullptr;
UsdStageRefPtr m_stage; UsdStageRefPtr m_stage;
@@ -88,12 +109,20 @@ private:
bool m_scrollToSelected = false; bool m_scrollToSelected = false;
/// Selected row in the Sublayers section (context menu / drag anchor).
int m_sublayerSelectedIdx = -1;
/// Stage-local layer identifiers rebuilt once per Render() for override detection. /// Stage-local layer identifiers rebuilt once per Render() for override detection.
/// Contains only the stage's own layers (root + sublayers + session), /// Contains only the stage's own layers (root + sublayers + session),
/// NOT layers that came in through references or payloads. /// NOT layers that came in through references or payloads.
std::unordered_set<std::string> m_localLayers; std::unordered_set<std::string> m_localLayers;
/// Visible prim order rebuilt each frame — used for Shift+RMB range select.
std::vector<pxr::SdfPath> m_visiblePrimOrder;
pxr::SdfPath m_rangeAnchorPath;
PrimSelectCallback m_onPrimSelected; PrimSelectCallback m_onPrimSelected;
MultiPrimSelectCallback m_onPrimsSelected;
StageMetadataChangedCallback m_onStageMetadataChanged; StageMetadataChangedCallback m_onStageMetadataChanged;
/// Remove-prim confirmation state. /// Remove-prim confirmation state.
@@ -104,6 +133,23 @@ private:
bool m_doReplaceRefPick = false; bool m_doReplaceRefPick = false;
SdfPath m_pendingReplaceRefPrim; SdfPath m_pendingReplaceRefPrim;
pxr::SdfReference m_pendingReplaceRef; pxr::SdfReference m_pendingReplaceRef;
/// Arc modal state (Inherit / Specialize / VariantSet / Variant).
ArcModalMode m_arcModalMode = ArcModalMode::None;
SdfPath m_arcModalTargetPrim;
std::string m_arcModalVarSetName;
char m_arcModalBuf[512] = {};
bool m_openArcModal = false;
/// Inline rename state.
SdfPath m_renamingPath; ///< non-empty while renaming
char m_renameBuf[256] = {};
bool m_renameJustStarted = false;
void HandleKeyboardShortcuts();
void ToggleSelectionFromClick(const std::string& path);
void RangeSelectToPath(const pxr::SdfPath& path);
SdfPath FindUniqueChildPath(const SdfPath& parent, const std::string& baseName);
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
+38 -6
View File
@@ -123,13 +123,18 @@ void StageEditorPanel::RenderFixedLayers(const std::vector<LayerInfo>& layers) {
ImGui::TableSetColumnIndex(3); ImGui::TableSetColumnIndex(3);
{ {
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
bool isET = li.isEditTarget; bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET) if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
: li.isEditTarget ? "Edit target (active)"
: "Set as edit target");
} }
ImGui::PopID(); ImGui::PopID();
@@ -242,6 +247,9 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
else else
ImGui::TextDisabled(" "); ImGui::TextDisabled(" ");
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
// Col 2: layer name // Col 2: layer name
ImGui::TableSetColumnIndex(2); ImGui::TableSetColumnIndex(2);
ImVec4 nameColor = li.isMuted ImVec4 nameColor = li.isMuted
@@ -250,6 +258,12 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
ImGui::TextColored(nameColor, "%s", li.displayName.c_str()); ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
if (ImGui::IsItemHovered() && !li.realPath.empty()) if (ImGui::IsItemHovered() && !li.realPath.empty())
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str()); ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
if (isRenderLayer) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
}
// Col 3: edit target checkbox — checking sets this layer as edit target; // Col 3: edit target checkbox — checking sets this layer as edit target;
// unchecking does nothing (there is always an active edit target). // unchecking does nothing (there is always an active edit target).
@@ -257,23 +271,32 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
{ {
bool isET = li.isEditTarget; bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET) if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
: li.isEditTarget ? "Edit target (active)"
: "Set as edit target");
} }
// Col 4: mute checkbox // Col 4: mute checkbox — disabled for render-layer sublayers, which
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
ImGui::TableSetColumnIndex(4); ImGui::TableSetColumnIndex(4);
{ {
bool muted = li.isMuted; bool muted = li.isMuted;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(isRenderLayer);
if (ImGui::Checkbox("##muted", &muted)) { if (ImGui::Checkbox("##muted", &muted)) {
if (muted) m_layerManager->MuteLayer(li.identifier); if (muted) m_layerManager->MuteLayer(li.identifier);
else m_layerManager->UnmuteLayer(li.identifier); else m_layerManager->UnmuteLayer(li.identifier);
} }
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
} }
ImGui::PopID(); ImGui::PopID();
@@ -289,14 +312,21 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
return; return;
const auto& li = sublayers[i]; const auto& li = sublayers[i];
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) { if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier); if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
else m_layerManager->MuteLayer(li.identifier); else m_layerManager->MuteLayer(li.identifier);
} }
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target")) if (!li.isEditTarget &&
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Separator(); ImGui::Separator();
@@ -337,13 +367,15 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Remove")) { if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
if (m_commandHistory) if (m_commandHistory)
m_commandHistory->Push( m_commandHistory->Push(
std::make_unique<LayerRemoveCommand>(m_layerManager, i)); std::make_unique<LayerRemoveCommand>(m_layerManager, i));
else else
m_layerManager->RemoveSublayer(i); m_layerManager->RemoveSublayer(i);
} }
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
ImGui::EndPopup(); ImGui::EndPopup();
} }
+8
View File
@@ -2,6 +2,7 @@
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h" #include "IconManager.h"
#include <imgui.h> #include <imgui.h>
#include <memory> #include <memory>
@@ -17,6 +18,12 @@ public:
void SetLayerManager(LayerManager* manager) { m_layerManager = manager; } void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; } void SetIconManager(IconManager* icons) { m_iconManager = icons; }
/// Optional. When set: sublayers RenderLayerManager created are badged
/// and have their mute/remove controls disabled here (they're managed
/// exclusively from the Render Layer panel so the two can't desync), and
/// every row's edit-target checkbox is disabled while a non-Default
/// render layer is active.
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void Render(); void Render();
private: private:
@@ -27,6 +34,7 @@ private:
LayerManager* m_layerManager = nullptr; LayerManager* m_layerManager = nullptr;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr; IconManager* m_iconManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
int m_selectedIdx = -1; int m_selectedIdx = -1;
}; };
+137
View File
@@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstring>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -35,6 +36,7 @@ void TimelinePanel::SetStage(pxr::UsdStageRefPtr stage)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void TimelinePanel::Update(float deltaTime) void TimelinePanel::Update(float deltaTime)
{ {
if (m_isPlayblasting) return; // time is stepped externally during capture
if (!m_playing) return; if (!m_playing) return;
double playStart = m_startFrame; double playStart = m_startFrame;
@@ -204,6 +206,117 @@ void TimelinePanel::Render()
ImGui::Text("%.6g fps", m_fps); ImGui::Text("%.6g fps", m_fps);
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Checkbox("Auto-Key", &m_autoKey)) NotifyTimeChanged(); if (ImGui::Checkbox("Auto-Key", &m_autoKey)) NotifyTimeChanged();
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// ---- Playblast button / progress ----
if (m_isPlayblasting) {
int total = m_playblastSettings.endFrame - m_playblastSettings.startFrame + 1;
ImGui::Text("Capturing %d / %d", m_playblastFrameIdx, total);
ImGui::SameLine();
if (ImGui::Button("Cancel##pb")) AbortPlayblast();
} else {
if (ImGui::Button("Playblast")) {
m_playblastSettings.startFrame = int(m_startFrame);
m_playblastSettings.endFrame = int(m_endFrame);
m_showPlayblastDialog = true;
}
}
// ---- Playblast settings dialog ----
if (m_showPlayblastDialog) {
ImGui::OpenPopup("Playblast##dlg");
m_showPlayblastDialog = false;
}
if (ImGui::BeginPopupModal("Playblast##dlg", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
// ── Output path ──────────────────────────────────────────────────────
ImGui::Text("Output directory:");
ImGui::SetNextItemWidth(340.f);
ImGui::InputText("##outdir", m_playblastSettings.outputDir,
sizeof(m_playblastSettings.outputDir));
ImGui::SameLine();
if (ImGui::Button("Browse...##outdir")) {
if (OnBrowseFolder) {
std::string dir = OnBrowseFolder();
if (!dir.empty()) {
strncpy(m_playblastSettings.outputDir, dir.c_str(),
sizeof(m_playblastSettings.outputDir) - 1);
m_playblastSettings.outputDir[sizeof(m_playblastSettings.outputDir) - 1] = '\0';
}
}
}
ImGui::Text("File prefix:");
ImGui::SetNextItemWidth(200.f);
ImGui::InputText("##prefix", m_playblastSettings.filePrefix,
sizeof(m_playblastSettings.filePrefix));
// ── Frame range ───────────────────────────────────────────────────────
ImGui::Separator();
ImGui::SetNextItemWidth(90.f);
ImGui::InputInt("Start frame##pb", &m_playblastSettings.startFrame);
ImGui::SetNextItemWidth(90.f);
ImGui::InputInt("End frame##pb", &m_playblastSettings.endFrame);
if (m_playblastSettings.endFrame < m_playblastSettings.startFrame)
m_playblastSettings.endFrame = m_playblastSettings.startFrame;
// ── Capture resolution ────────────────────────────────────────────────
ImGui::Separator();
ImGui::Text("Capture resolution:");
ImGui::SetNextItemWidth(80.f);
ImGui::InputInt("W##pbw", &m_playblastSettings.captureWidth);
if (m_playblastSettings.captureWidth < 16) m_playblastSettings.captureWidth = 16;
ImGui::SameLine();
ImGui::Text("x");
ImGui::SameLine();
ImGui::SetNextItemWidth(80.f);
ImGui::InputInt("H##pbh", &m_playblastSettings.captureHeight);
if (m_playblastSettings.captureHeight < 16) m_playblastSettings.captureHeight = 16;
// Common presets
ImGui::SameLine();
if (ImGui::Button("HD")) { m_playblastSettings.captureWidth = 1280; m_playblastSettings.captureHeight = 720; }
ImGui::SameLine();
if (ImGui::Button("FHD")) { m_playblastSettings.captureWidth = 1920; m_playblastSettings.captureHeight = 1080; }
ImGui::SameLine();
if (ImGui::Button("4K")) { m_playblastSettings.captureWidth = 3840; m_playblastSettings.captureHeight = 2160; }
// ── Movie export ──────────────────────────────────────────────────────
ImGui::Separator();
ImGui::Checkbox("Export movie (H.264 MP4 via libavcodec)", &m_playblastSettings.exportMovie);
if (m_playblastSettings.exportMovie) {
ImGui::Indent();
ImGui::Checkbox("Also save BMP frames alongside movie", &m_playblastSettings.keepFrames);
ImGui::Unindent();
}
ImGui::Separator();
ImGui::TextDisabled("Writes: <dir>\\<prefix>.NNNN.bmp%s",
m_playblastSettings.exportMovie ? " → <prefix>.mp4" : "");
// ── Buttons ───────────────────────────────────────────────────────────
ImGui::Spacing();
if (ImGui::Button("Cancel##dlg")) ImGui::CloseCurrentPopup();
ImGui::SameLine();
bool canStart = m_playblastSettings.outputDir[0] != '\0' &&
m_playblastSettings.filePrefix[0] != '\0';
ImGui::BeginDisabled(!canStart);
if (ImGui::Button("Start##dlg")) {
ImGui::CloseCurrentPopup();
m_playblastWasPlaying = m_playing;
m_playing = false;
m_playblastSettings.fps = m_fps;
m_playblastFrame = double(m_playblastSettings.startFrame);
m_playblastFrameIdx = 0;
m_playblastSkipFirst = true;
m_isPlayblasting = true;
SetCurrentFrame(m_playblastFrame);
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
// ---- Slider row: [start] [----slider----] [end] ---- // ---- Slider row: [start] [----slider----] [end] ----
{ {
@@ -256,6 +369,11 @@ void TimelinePanel::SetCurrentFrame(double frame)
} }
} }
void TimelinePanel::SetCurrentFrameExternal(double frame)
{
SetCurrentFrame(frame);
}
void TimelinePanel::NotifyTimeChanged() void TimelinePanel::NotifyTimeChanged()
{ {
if (OnTimeChanged) if (OnTimeChanged)
@@ -268,4 +386,23 @@ pxr::UsdTimeCode TimelinePanel::EditTime() const
: pxr::UsdTimeCode::Default(); : pxr::UsdTimeCode::Default();
} }
// ---------------------------------------------------------------------------
bool TimelinePanel::AdvancePlayblast()
{
m_playblastFrame += 1.0;
m_playblastFrameIdx++;
if (m_playblastFrame > double(m_playblastSettings.endFrame)) {
AbortPlayblast();
return false;
}
SetCurrentFrame(m_playblastFrame);
return true;
}
void TimelinePanel::AbortPlayblast()
{
m_isPlayblasting = false;
m_playing = m_playblastWasPlaying;
}
} // namespace UsdLayerManager } // namespace UsdLayerManager
+41
View File
@@ -30,6 +30,38 @@ public:
std::function<void(pxr::UsdTimeCode displayTime, std::function<void(pxr::UsdTimeCode displayTime,
pxr::UsdTimeCode editTime)> OnTimeChanged; pxr::UsdTimeCode editTime)> OnTimeChanged;
// ── Playblast ────────────────────────────────────────────────────────────
struct PlayblastSettings {
char outputDir[512] = "";
char filePrefix[128] = "playblast";
int startFrame = 1;
int endFrame = 100;
int captureWidth = 1920;
int captureHeight = 1080;
bool exportMovie = true; // encode directly to MP4 (libavcodec)
bool keepFrames = false; // also write BMP frames alongside movie
double fps = 24.0; // filled from stage fps at capture start
};
bool IsPlayblasting() const { return m_isPlayblasting; }
/// True on the frame immediately after playblast begins — the viewport has
/// not yet rendered at the start time, so the first capture must be skipped.
bool GetPlayblastSkipFirst() const { return m_playblastSkipFirst; }
void ClearPlayblastSkipFirst() { m_playblastSkipFirst = false; }
int GetPlayblastFrameIndex() const { return m_playblastFrameIdx; }
const PlayblastSettings& GetPlayblastSettings() const { return m_playblastSettings; }
double GetFps() const { return m_fps; }
/// Step to the next capture frame. Returns false (and stops capture) when done.
bool AdvancePlayblast();
void AbortPlayblast();
/// Drive the timeline from an external source (e.g. curve editor scrubbing).
void SetCurrentFrameExternal(double frame);
/// Callback set by Application so the dialog can open a native folder picker.
std::function<std::string()> OnBrowseFolder;
private: private:
/// Clamp to [startFrame, endFrame]; fires OnTimeChanged when the frame changes. /// Clamp to [startFrame, endFrame]; fires OnTimeChanged when the frame changes.
void SetCurrentFrame(double frame); void SetCurrentFrame(double frame);
@@ -54,6 +86,15 @@ private:
// Auto-Key. // Auto-Key.
bool m_autoKey = false; bool m_autoKey = false;
// Playblast state.
bool m_isPlayblasting = false;
bool m_playblastSkipFirst = false;
double m_playblastFrame = 0.0;
int m_playblastFrameIdx = 0;
bool m_playblastWasPlaying = false;
PlayblastSettings m_playblastSettings;
bool m_showPlayblastDialog = false;
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
+79 -16
View File
@@ -366,7 +366,11 @@ void TransformManipulator::DrawScaleGizmo(ImDrawList* dl, const pxr::GfMatrix4d&
dl->AddLine(pivSS,tipSS,col,kScaleLineThick); dl->AddLine(pivSS,tipSS,col,kScaleLineThick);
dl->AddCircleFilled(tipSS,kScaleCircleRadius,col,16); dl->AddCircleFilled(tipSS,kScaleCircleRadius,col,16);
} }
dl->AddCircleFilled(pivSS,kCenterCircleRadius+1.f,kColCenter,16); // Center dot: axis 3 = uniform scale handle
bool centerActive = (m_dragAxis==3||m_hoveredAxis==3);
ImU32 centerCol = centerActive ? kColHover : kColCenter;
float centerR = centerActive ? kCenterCircleRadius+3.f : kCenterCircleRadius+1.f;
dl->AddCircleFilled(pivSS,centerR,centerCol,16);
} }
// ============================================================================ // ============================================================================
@@ -431,9 +435,25 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
// Hover update // Hover update
if (!m_isDragging && hovered) { if (!m_isDragging && hovered) {
m_hoveredAxis=(m_mode==ManipulatorMode::Rotate) if (m_mode==ManipulatorMode::Rotate) {
?HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes) m_hoveredAxis=HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes);
:HitTestAxes (vp,pivot,sf, imgPos,vW,vH,mouse,axes); } else {
// Scale center check FIRST: the pivot is an endpoint of all three
// axis segments, so HitTestAxes always hits an axis (typically X)
// when the mouse is at the center. Check the center dot before
// falling through to the per-axis test.
m_hoveredAxis=-1;
if (m_mode==ManipulatorMode::Scale) {
ImVec2 pivSS;
if (WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) {
float dx=mouse.x-pivSS.x, dy=mouse.y-pivSS.y;
if (std::sqrt(dx*dx+dy*dy)<kCenterCircleRadius+6.f)
m_hoveredAxis=3;
}
}
if (m_hoveredAxis<0)
m_hoveredAxis=HitTestAxes(vp,pivot,sf,imgPos,vW,vH,mouse,axes);
}
} }
bool consumed=false; bool consumed=false;
@@ -444,9 +464,22 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
if (hovered && !m_isDragging && if (hovered && !m_isDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt) ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt)
{ {
int hit=(m_mode==ManipulatorMode::Rotate) int hit=-1;
?HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes) if (m_mode==ManipulatorMode::Rotate) {
:HitTestAxes (vp,pivot,sf, imgPos,vW,vH,mouse,axes); hit=HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes);
} else {
// Scale center check before per-axis test (same reason as hover).
if (m_mode==ManipulatorMode::Scale) {
ImVec2 pivSS;
if (WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) {
float dx=mouse.x-pivSS.x, dy=mouse.y-pivSS.y;
if (std::sqrt(dx*dx+dy*dy)<kCenterCircleRadius+6.f)
hit=3;
}
}
if (hit<0)
hit=HitTestAxes(vp,pivot,sf,imgPos,vW,vH,mouse,axes);
}
if (hit>=0) { if (hit>=0) {
m_isDragging=true; m_dragAxis=hit; consumed=true; m_isDragging=true; m_dragAxis=hit; consumed=true;
@@ -513,10 +546,19 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
m_dragRotateObjectSpace=(m_transformSpace==TransformSpace::Object); m_dragRotateObjectSpace=(m_transformSpace==TransformSpace::Object);
} }
else if (m_mode==ManipulatorMode::Scale) { else if (m_mode==ManipulatorMode::Scale) {
// usdtweak: _axisLine = GfLine(pivot, objectTransform.GetRow3(axis)) if (m_dragAxis<3) {
// Single-axis scale: project onto world-space axis line
m_dragScaleAxisLine=pxr::GfLine(pivot,axes[m_dragAxis]); m_dragScaleAxisLine=pxr::GfLine(pivot,axes[m_dragAxis]);
pxr::GfVec3d rpt; pxr::GfVec3d rpt;
pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&m_dragScaleOriginOnAxis); pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&m_dragScaleOriginOnAxis);
} else {
// Uniform scale (axis 3): mouse starts at the center circle,
// so the axis-line origin parameter oT ≈ 0 and cT/oT blows up.
// Use screen-space pixel delta instead: store drag-start mouse
// position in m_dragScaleOriginOnAxis.x/y, compute ratio as
// 1 + deltaX * sensitivity each frame.
m_dragScaleOriginOnAxis={double(mouse.x),double(mouse.y),0.0};
}
} }
} }
} }
@@ -527,7 +569,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
if (m_isDragging) { if (m_isDragging) {
consumed=true; consumed=true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (m_dragAxis<0||m_dragAxis>2) { m_isDragging=false;m_dragAxis=-1;return consumed; } if (m_dragAxis<0||m_dragAxis>3) { m_isDragging=false;m_dragAxis=-1;return consumed; }
pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH); pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH);
@@ -603,11 +645,21 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
? deltaM * m_dragRotateInitialRotMat ? deltaM * m_dragRotateInitialRotMat
: m_dragRotateInitialRotMat * deltaM; : m_dragRotateInitialRotMat * deltaM;
// usdtweak: GfRotation::DecomposeRotation(resultingRotation, // Decompose `result` (the new total local rotation matrix) back
// pxAxis, pyAxis, pzAxis, 1.0, &tw, &fb, &lr, &sw, true) // into XformCommonAPI Euler angles.
const pxr::GfVec3d pxA=m_dragRotateInitialRotMat.GetRow3(0); //
const pxr::GfVec3d pyA=m_dragRotateInitialRotMat.GetRow3(1); // SetRotate() interprets the written angles as rotations about the
const pxr::GfVec3d pzA=m_dragRotateInitialRotMat.GetRow3(2); // CANONICAL x/y/z axes, so the decompose axes MUST be canonical.
// The previous port used the rows of initRot here -- i.e. the prim's
// already-rotated frame -- which equals the canonical axes only when
// the prim is at zero rotation. That mismatch is exactly why the
// authored result desynced from the manipulator once the prim already
// carried a rotation: the angles were measured in the rotated frame
// but written as if they were canonical-frame angles.
//
// (Decomposition assumes RotationOrderXYZ -- the XformCommonAPI
// default; non-XYZ orders are mapped tw->X, fb->Y, lr->Z as before.)
static const pxr::GfVec3d kCanonX(1,0,0), kCanonY(0,1,0), kCanonZ(0,0,1);
double tw=pxr::GfDegreesToRadians(double(m_dragStartRotate[0])); double tw=pxr::GfDegreesToRadians(double(m_dragStartRotate[0]));
double fb=pxr::GfDegreesToRadians(double(m_dragStartRotate[1])); double fb=pxr::GfDegreesToRadians(double(m_dragStartRotate[1]));
@@ -615,7 +667,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
double sw=0; double sw=0;
pxr::GfRotation::DecomposeRotation( pxr::GfRotation::DecomposeRotation(
result,pxA,pyA,pzA,1.0,&tw,&fb,&lr,&sw,/*useHint=*/true); result,kCanonX,kCanonY,kCanonZ,1.0,&tw,&fb,&lr,&sw,/*useHint=*/true);
pxr::GfVec3f newRot(float(pxr::GfRadiansToDegrees(tw)), pxr::GfVec3f newRot(float(pxr::GfRadiansToDegrees(tw)),
float(pxr::GfRadiansToDegrees(fb)), float(pxr::GfRadiansToDegrees(fb)),
@@ -632,14 +684,25 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
// robust when the pivot is not at the world origin. // robust when the pivot is not at the world origin.
// ----------------------------------------------------------------- // -----------------------------------------------------------------
else if (m_mode==ManipulatorMode::Scale) { else if (m_mode==ManipulatorMode::Scale) {
pxr::GfVec3f ns=m_dragOriginalScale;
if (m_dragAxis<3) {
// Single-axis: project onto world-space axis line.
pxr::GfVec3d rpt,cur; pxr::GfVec3d rpt,cur;
pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&cur); pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&cur);
double oT=0,cT=0; double oT=0,cT=0;
m_dragScaleAxisLine.FindClosestPoint(m_dragScaleOriginOnAxis,&oT); m_dragScaleAxisLine.FindClosestPoint(m_dragScaleOriginOnAxis,&oT);
m_dragScaleAxisLine.FindClosestPoint(cur,&cT); m_dragScaleAxisLine.FindClosestPoint(cur,&cT);
double ratio=(std::abs(oT)>1e-6)?cT/oT:1.0; double ratio=(std::abs(oT)>1e-6)?cT/oT:1.0;
pxr::GfVec3f ns=m_dragOriginalScale;
ns[m_dragAxis]=std::max(0.001f,float(double(m_dragOriginalScale[m_dragAxis])*ratio)); ns[m_dragAxis]=std::max(0.001f,float(double(m_dragOriginalScale[m_dragAxis])*ratio));
} else {
// Uniform scale: screen-space pixel delta → ratio.
// Right drag = grow, left drag = shrink.
// 200px right ≈ ×2 (sensitivity 0.005 per pixel).
double deltaX=double(mouse.x)-m_dragScaleOriginOnAxis[0];
double ratio=std::max(0.001,1.0+deltaX*0.005);
for (int i=0;i<3;++i)
ns[i]=std::max(0.001f,float(double(m_dragOriginalScale[i])*ratio));
}
ApplyScale(ns); m_dragStartScale=ns; ApplyScale(ns); m_dragStartScale=ns;
} }
+175 -3
View File
@@ -3,6 +3,8 @@
#include <imgui.h> #include <imgui.h>
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include <fstream>
#include <sstream>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -65,6 +67,7 @@ void ViewportPanel::WireCallbacks(int i)
BroadcastSelection(); BroadcastSelection();
if (OnPrimsPickedRect) OnPrimsPickedRect(paths); if (OnPrimsPickedRect) OnPrimsPickedRect(paths);
}; };
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -135,6 +138,17 @@ void ViewportPanel::SetSelectedPrimPath(const std::string& path)
BroadcastSelection(); BroadcastSelection();
} }
void ViewportPanel::SetSelectedPrimPaths(const std::vector<std::string>& paths)
{
m_selectedSdfPaths.clear();
m_selectedPrimPath.clear();
for (const auto& p : paths)
m_selectedSdfPaths.push_back(pxr::SdfPath(p));
if (!paths.empty())
m_selectedPrimPath = paths.front();
BroadcastSelection();
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Forwarding accessors // Forwarding accessors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -233,6 +247,12 @@ bool ViewportPanel::IsMouseOverDivider(ImVec2 origin, ImVec2 total) const
if (m_maximizedTileIndex >= 0) return false; if (m_maximizedTileIndex >= 0) return false;
if (m_draggingDivH || m_draggingDivV) return true; if (m_draggingDivH || m_draggingDivV) return true;
// The hit-zones below are raw screen rects; without this gate a floating
// window stacked over the viewport (e.g. the Material Editor) wouldn't
// mask them and the divider would still react underneath it.
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
return false;
const float kDivHalf = 3.0f; const float kDivHalf = 3.0f;
ImVec2 mouse = ImGui::GetMousePos(); ImVec2 mouse = ImGui::GetMousePos();
@@ -269,6 +289,11 @@ void ViewportPanel::DrawDividers(ImVec2 origin, ImVec2 total)
// accidentally triggered when the mouse drifts over the hit-zone. // accidentally triggered when the mouse drifts over the hit-zone.
bool lmbClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); bool lmbClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool lmbReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); bool lmbReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
// Raw screen-rect hit-testing below — require this window (or its tiles)
// to actually be the hovered window, so a floating window stacked on top
// (e.g. the Material Editor) masks the dividers. In-progress drags are
// exempt: they must keep tracking until LMB release wherever the mouse is.
bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
// Vertical divider (HSplit / Quad) // Vertical divider (HSplit / Quad)
if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) { if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) {
@@ -276,7 +301,7 @@ void ViewportPanel::DrawDividers(ImVec2 origin, ImVec2 total)
ImVec2 hMin(divX - kDivThick * 0.5f, origin.y); ImVec2 hMin(divX - kDivThick * 0.5f, origin.y);
ImVec2 hMax(divX + kDivThick * 0.5f, origin.y + total.y); ImVec2 hMax(divX + kDivThick * 0.5f, origin.y + total.y);
bool hovering = !m_draggingDivV && bool hovering = windowHovered && !m_draggingDivV &&
mousePos.x >= hMin.x && mousePos.x <= hMax.x && mousePos.x >= hMin.x && mousePos.x <= hMax.x &&
mousePos.y >= hMin.y && mousePos.y <= hMax.y; mousePos.y >= hMin.y && mousePos.y <= hMax.y;
@@ -301,7 +326,7 @@ void ViewportPanel::DrawDividers(ImVec2 origin, ImVec2 total)
ImVec2 hMin(origin.x, divY - kDivThick * 0.5f); ImVec2 hMin(origin.x, divY - kDivThick * 0.5f);
ImVec2 hMax(origin.x + total.x, divY + kDivThick * 0.5f); ImVec2 hMax(origin.x + total.x, divY + kDivThick * 0.5f);
bool hovering = !m_draggingDivH && bool hovering = windowHovered && !m_draggingDivH &&
mousePos.x >= hMin.x && mousePos.x <= hMax.x && mousePos.x >= hMin.x && mousePos.x <= hMax.x &&
mousePos.y >= hMin.y && mousePos.y <= hMax.y; mousePos.y >= hMin.y && mousePos.y <= hMax.y;
@@ -381,7 +406,7 @@ void ViewportPanel::RenderGlobalLeftToolbar(ImVec2 contentPos, ImVec2 /*contentS
const float kIconPad = 5.0f; const float kIconPad = 5.0f;
const float kRounding = 4.0f; const float kRounding = 4.0f;
const float kSpacing = 3.0f; const float kSpacing = 3.0f;
const float kPadX = 9.0f; // left padding inside the strip const float kPadX = 4.0f; // left padding inside the strip
const float kPadY = 10.0f; // top padding const float kPadY = 10.0f; // top padding
const float kSepH = 1.0f; // separator line height const float kSepH = 1.0f; // separator line height
const float kSepGap = 6.0f; // space around separator const float kSepGap = 6.0f; // space around separator
@@ -608,4 +633,151 @@ void ViewportPanel::Render(bool* p_open)
ImGui::PopStyleVar(); // outer WindowPadding ImGui::PopStyleVar(); // outer WindowPadding
} }
// ---------------------------------------------------------------------------
// Global preferences
// ---------------------------------------------------------------------------
void ViewportPanel::ApplyDefaultDelegate(const std::string& delegate)
{
if (delegate.empty()) return;
for (auto& tile : m_tiles) {
if (tile->GetRenderer().GetCurrentRendererId().IsEmpty())
tile->GetRenderer().SetRendererPlugin(pxr::TfToken(delegate));
}
}
void ViewportPanel::ApplyGlobalColorCorrection(int ccMode,
const std::string& ocioDisplay,
const std::string& ocioView,
const std::string& ocioColorSpace,
const std::string& ocioLook)
{
for (auto& tile : m_tiles) {
auto& r = tile->GetRenderer();
r.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(ccMode));
r.SetOcioDisplay(ocioDisplay);
r.SetOcioView(ocioView);
r.SetOcioColorSpace(ocioColorSpace);
r.SetOcioLook(ocioLook);
}
}
// ---------------------------------------------------------------------------
// Settings persistence
// ---------------------------------------------------------------------------
void ViewportPanel::SaveSettings(const std::string& path) const
{
std::ofstream f(path);
if (!f) return;
f << "[Viewport]\n";
f << "Layout=" << static_cast<int>(m_layout) << "\n";
f << "SplitH=" << m_splitH << "\n";
f << "SplitV=" << m_splitV << "\n";
for (int i = 0; i < static_cast<int>(m_tiles.size()); ++i) {
ViewportTileSettings s = m_tiles[i]->GetSettings();
f << "\n[Tile" << i << "]\n";
f << "RenderDelegate=" << s.renderDelegate << "\n";
f << "ShowGrid=" << (s.showGrid ? 1 : 0) << "\n";
f << "ShowCameraGuide=" << (s.showCameraGuide ? 1 : 0) << "\n";
f << "Aov=" << s.aov << "\n";
f << "ShowGuides=" << (s.showGuides ? 1 : 0) << "\n";
f << "ShowProxy=" << (s.showProxy ? 1 : 0) << "\n";
f << "ShowRender=" << (s.showRender ? 1 : 0) << "\n";
f << "AAEnabled=" << (s.aaEnabled ? 1 : 0) << "\n";
f << "BgColorR=" << s.bgColorR << "\n";
f << "BgColorG=" << s.bgColorG << "\n";
f << "BgColorB=" << s.bgColorB << "\n";
f << "BBoxMode=" << s.bboxMode << "\n";
f << "AmbientLightOnly=" << (s.ambientLightOnly ? 1 : 0) << "\n";
f << "DomeLightEnabled=" << (s.domeLightEnabled ? 1 : 0) << "\n";
f << "ShadingMode=" << s.shadingMode << "\n";
f << "CullStyle=" << s.cullStyle << "\n";
}
}
void ViewportPanel::LoadSettings(const std::string& path)
{
std::ifstream f(path);
if (!f) return;
int layoutMode = static_cast<int>(m_layout);
float splitH = m_splitH;
float splitV = m_splitV;
std::vector<ViewportTileSettings> tileSettings; // indexed by tile number
int section = -1; // -1=Viewport, >=0 = tile index
std::string line;
while (std::getline(f, line)) {
// Strip leading whitespace and CR
while (!line.empty() && (line.front() == ' ' || line.front() == '\t'))
line.erase(line.begin());
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line.empty() || line.front() == '#') continue;
if (line.front() == '[') {
size_t close = line.find(']');
if (close == std::string::npos) continue;
std::string name = line.substr(1, close - 1);
if (name == "Viewport") {
section = -1;
} else if (name.size() > 4 && name.substr(0, 4) == "Tile") {
try {
int idx = std::stoi(name.substr(4));
if (idx >= 0) {
section = idx;
if (static_cast<int>(tileSettings.size()) <= idx)
tileSettings.resize(idx + 1);
}
} catch (...) { section = -1; }
}
continue;
}
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
auto toInt = [](const std::string& v, int def) { try { return std::stoi(v); } catch (...) { return def; } };
auto toFloat = [](const std::string& v, float def) { try { return std::stof(v); } catch (...) { return def; } };
if (section == -1) {
if (key == "Layout") layoutMode = toInt(val, layoutMode);
else if (key == "SplitH") splitH = toFloat(val, splitH);
else if (key == "SplitV") splitV = toFloat(val, splitV);
} else {
auto& ts = tileSettings[section];
if (key == "RenderDelegate") ts.renderDelegate = val;
else if (key == "ShowGrid") ts.showGrid = (val == "1");
else if (key == "ShowCameraGuide") ts.showCameraGuide = (val == "1");
else if (key == "Aov") ts.aov = val;
else if (key == "ShowGuides") ts.showGuides = (val == "1");
else if (key == "ShowProxy") ts.showProxy = (val == "1");
else if (key == "ShowRender") ts.showRender = (val == "1");
else if (key == "AAEnabled") ts.aaEnabled = (val == "1");
else if (key == "BgColorR") ts.bgColorR = toFloat(val, ts.bgColorR);
else if (key == "BgColorG") ts.bgColorG = toFloat(val, ts.bgColorG);
else if (key == "BgColorB") ts.bgColorB = toFloat(val, ts.bgColorB);
else if (key == "BBoxMode") ts.bboxMode = toInt(val, ts.bboxMode);
else if (key == "AmbientLightOnly") ts.ambientLightOnly = (val == "1");
else if (key == "DomeLightEnabled") ts.domeLightEnabled = (val == "1");
else if (key == "ShadingMode") ts.shadingMode = toInt(val, ts.shadingMode);
else if (key == "CullStyle") ts.cullStyle = toInt(val, ts.cullStyle);
}
}
// Apply layout (creates/destroys tiles as needed)
m_splitH = splitH;
m_splitV = splitV;
SetLayout(static_cast<LayoutMode>(std::clamp(layoutMode, 0, 3)));
// Apply per-tile settings to whichever tiles now exist
for (int i = 0; i < static_cast<int>(tileSettings.size()); ++i) {
if (i < static_cast<int>(m_tiles.size()))
m_tiles[i]->ApplySettings(tileSettings[i]);
}
}
} // namespace UsdLayerManager } // namespace UsdLayerManager
+20 -2
View File
@@ -48,11 +48,13 @@ public:
// ── Selection (called by SceneHierarchyPanel) ──────────────────────────── // ── Selection (called by SceneHierarchyPanel) ────────────────────────────
/// Set a single selected prim (clears any multi-selection). /// Set a single selected prim (clears any multi-selection).
void SetSelectedPrimPath(const std::string& path); void SetSelectedPrimPath(const std::string& path);
/// Set multiple selected prims (from hierarchy Ctrl+click multi-select).
void SetSelectedPrimPaths(const std::vector<std::string>& paths);
// ── Main render (called from Application::RenderUI) ────────────────────── // ── Main render (called from Application::RenderUI) ──────────────────────
void Render(bool* p_open = nullptr); void Render(bool* p_open = nullptr);
// ── Pick callbacks (wired by Application after construction) ───────────── // ── Callbacks (wired by Application after construction) ──────────────────
std::function<void(const std::string&)> OnPrimPicked; std::function<void(const std::string&)> OnPrimPicked;
std::function<void(const std::vector<std::string>&)> OnPrimsPickedRect; std::function<void(const std::vector<std::string>&)> OnPrimsPickedRect;
@@ -65,6 +67,22 @@ public:
LayoutMode GetLayout() const { return m_layout; } LayoutMode GetLayout() const { return m_layout; }
int GetFocusedTileIndex() const { return m_focusedTileIndex; } int GetFocusedTileIndex() const { return m_focusedTileIndex; }
// ── Global preferences ────────────────────────────────────────────────────
/// Set render delegate on tiles that have no saved delegate yet (empty token).
/// Called once on startup so the pref default applies to fresh tiles.
void ApplyDefaultDelegate(const std::string& delegate);
/// Apply color correction to every tile (always global).
void ApplyGlobalColorCorrection(int ccMode, const std::string& ocioDisplay,
const std::string& ocioView,
const std::string& ocioColorSpace,
const std::string& ocioLook);
// ── Settings persistence ─────────────────────────────────────────────────
/// Load layout and per-tile render settings from a simple INI file.
void LoadSettings(const std::string& path);
/// Save current layout and per-tile render settings to a simple INI file.
void SaveSettings(const std::string& path) const;
private: private:
// ── Tile rect helper ───────────────────────────────────────────────────── // ── Tile rect helper ─────────────────────────────────────────────────────
@@ -83,7 +101,7 @@ private:
bool IsMouseOverDivider(ImVec2 origin, ImVec2 total) const; bool IsMouseOverDivider(ImVec2 origin, ImVec2 total) const;
/// Width (px) of the reserved left toolbar strip. /// Width (px) of the reserved left toolbar strip.
static constexpr float kToolbarW = 52.0f; static constexpr float kToolbarW = 40.0f;
// ── Selection management ───────────────────────────────────────────────── // ── Selection management ─────────────────────────────────────────────────
/// Push the current shared selection into every tile and the manipulator. /// Push the current shared selection into every tile and the manipulator.
+341 -8
View File
@@ -16,6 +16,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <map>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -87,6 +88,48 @@ void ViewportTile::FrameScene()
} }
} }
// ---------------------------------------------------------------------------
ViewportTileSettings ViewportTile::GetSettings() const
{
ViewportTileSettings s;
s.renderDelegate = m_renderer.GetCurrentRendererId().GetString();
s.showGrid = m_renderer.ShowGrid();
s.showCameraGuide = m_renderer.ShowCameraGuide();
s.aov = m_renderer.GetCurrentAov().GetString();
s.showGuides = m_renderer.ShowGuides();
s.showProxy = m_renderer.ShowProxy();
s.showRender = m_renderer.ShowRender();
s.aaEnabled = m_renderer.GetAAEnabled();
const pxr::GfVec3f& bg = m_renderer.GetBackgroundColor();
s.bgColorR = bg[0]; s.bgColorG = bg[1]; s.bgColorB = bg[2];
s.bboxMode = static_cast<int>(m_renderer.GetBBoxMode());
s.ambientLightOnly = m_renderer.GetAmbientLightOnly();
s.domeLightEnabled = m_renderer.GetDomeLightEnabled();
s.shadingMode = static_cast<int>(m_renderer.GetShadingMode());
s.cullStyle = static_cast<int>(m_renderer.GetCullStyle());
return s;
}
void ViewportTile::ApplySettings(const ViewportTileSettings& s)
{
if (!s.renderDelegate.empty())
m_renderer.SetRendererPlugin(pxr::TfToken(s.renderDelegate));
m_renderer.SetShowGrid(s.showGrid);
m_renderer.SetShowCameraGuide(s.showCameraGuide);
if (!s.aov.empty())
m_renderer.SetCurrentAov(pxr::TfToken(s.aov));
m_renderer.SetShowGuides(s.showGuides);
m_renderer.SetShowProxy(s.showProxy);
m_renderer.SetShowRender(s.showRender);
m_renderer.SetAAEnabled(s.aaEnabled);
m_renderer.SetBackgroundColor(pxr::GfVec3f(s.bgColorR, s.bgColorG, s.bgColorB));
m_renderer.SetBBoxMode(static_cast<BBoxMode>(s.bboxMode));
m_renderer.SetAmbientLightOnly(s.ambientLightOnly);
m_renderer.SetDomeLightEnabled(s.domeLightEnabled);
m_renderer.SetShadingMode(static_cast<ShadingMode>(s.shadingMode));
m_renderer.SetCullStyle(static_cast<CullStyle>(s.cullStyle));
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void ViewportTile::RefreshCameraList() void ViewportTile::RefreshCameraList()
{ {
@@ -709,6 +752,10 @@ void ViewportTile::RenderContextMenu(int tileIndex)
if (ImGui::MenuItem("Show Grid", nullptr, &showGrid)) if (ImGui::MenuItem("Show Grid", nullptr, &showGrid))
m_renderer.SetShowGrid(showGrid); m_renderer.SetShowGrid(showGrid);
bool showGuide = m_renderer.ShowCameraGuide();
if (ImGui::MenuItem("Camera Guide", nullptr, &showGuide))
m_renderer.SetShowCameraGuide(showGuide);
ImGui::Separator(); ImGui::Separator();
bool ambientOnly = m_renderer.GetAmbientLightOnly(); bool ambientOnly = m_renderer.GetAmbientLightOnly();
@@ -871,30 +918,32 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
ImGui::TextDisabled("|"); ImGui::TextDisabled("|");
ImGui::SameLine(); ImGui::SameLine();
// -- Render delegate ----------------------------------------------------- // -- Render delegate (global) --------------------------------------------
{
pxr::TfToken currentId = m_renderer.GetCurrentRendererId(); pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
std::string displayName = currentId.IsEmpty() std::string displayName = currentId.IsEmpty()
? "Rdr" ? "Rdr"
: UsdSceneRenderer::GetRendererDisplayName(currentId); : UsdSceneRenderer::GetRendererDisplayName(currentId);
if (displayName.size() > 8) displayName = displayName.substr(0, 8); if (displayName.size() > 8) displayName = displayName.substr(0, 8);
std::string rdrBtnId = "##Rdr_" + std::to_string(tileIndex); std::string rdrPopupId = "RdrPopup_" + std::to_string(tileIndex);
ImGui::Button(displayName.c_str(), ImVec2(72.0f, 0.0f)); ImGui::Button(displayName.c_str(), ImVec2(72.0f, 0.0f));
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Render delegate -- click to change"); ImGui::SetTooltip("Render delegate");
ImGuiPopupFlags popupFlags = ImGuiPopupFlags_MouseButtonLeft; if (ImGui::BeginPopupContextItem(rdrPopupId.c_str(),
std::string rdrPopupId = "RdrPopup_" + std::to_string(tileIndex); ImGuiPopupFlags_MouseButtonLeft))
if (ImGui::BeginPopupContextItem(rdrPopupId.c_str(), popupFlags)) { {
for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) { for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId); std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
if (name.empty()) name = pluginId.GetString(); if (name.empty()) name = pluginId.GetString();
bool selected = (pluginId == currentId); bool selected = (pluginId == currentId);
if (ImGui::MenuItem(name.c_str(), nullptr, selected)) if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
if (!selected) m_renderer.SetRendererPlugin(pluginId); m_renderer.SetRendererPlugin(pluginId);
} }
ImGui::EndPopup(); ImGui::EndPopup();
} }
}
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::TextDisabled("|");
@@ -936,6 +985,150 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
if (iconToggle(aaId.c_str(), "AA", Icon::Antialias, aa, "Toggle anti-aliasing")) if (iconToggle(aaId.c_str(), "AA", Icon::Antialias, aa, "Toggle anti-aliasing"))
m_renderer.SetAAEnabled(!aa); m_renderer.SetAAEnabled(!aa);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// -- Gear / viewport options popup ---------------------------------------
{
std::string gearId = "##gear_" + std::to_string(tileIndex);
std::string gearPopupId = "GearPopup_" + std::to_string(tileIndex);
if (m_iconManager) {
ImTextureID tex = m_iconManager->Get(Icon::Settings);
ImGui::ImageButton(gearId.c_str(), ImTextureRef(tex), ImVec2(16.f, 16.f));
} else {
ImGui::Button("*");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Viewport options");
if (ImGui::BeginPopupContextItem(gearPopupId.c_str(),
ImGuiPopupFlags_MouseButtonLeft))
{
// -- Shading mode ------------------------------------------------
if (ImGui::BeginMenu("Shading")) {
ShadingMode cur = m_renderer.GetShadingMode();
auto shadingItem = [&](const char* label, ShadingMode mode) {
if (ImGui::MenuItem(label, nullptr, cur == mode))
m_renderer.SetShadingMode(mode);
};
shadingItem("Smooth Shaded", ShadingMode::SmoothShaded);
shadingItem("Flat Shaded", ShadingMode::FlatShaded);
shadingItem("Wireframe on Surface", ShadingMode::WireframeOnSurface);
shadingItem("Wireframe", ShadingMode::Wireframe);
shadingItem("Unlit", ShadingMode::Unlit);
ImGui::EndMenu();
}
// -- Cull Style --------------------------------------------------
if (ImGui::BeginMenu("Cull Style")) {
CullStyle curCull = m_renderer.GetCullStyle();
auto cullItem = [&](const char* label, CullStyle style) {
if (ImGui::MenuItem(label, nullptr, curCull == style))
m_renderer.SetCullStyle(style);
};
cullItem("Back Unless Double-Sided", CullStyle::BackUnlessDoubleSided);
cullItem("Back", CullStyle::Back);
cullItem("Front", CullStyle::Front);
cullItem("Nothing", CullStyle::Nothing);
cullItem("No Opinion", CullStyle::NoOpinion);
ImGui::EndMenu();
}
// -- AOV ---------------------------------------------------------
{
pxr::TfTokenVector aovs = m_renderer.GetRendererAovs();
if (ImGui::BeginMenu("AOV", !aovs.empty())) {
pxr::TfToken currentAov = m_renderer.GetCurrentAov();
for (const auto& aov : aovs) {
bool checked = (aov == currentAov);
if (ImGui::MenuItem(aov.GetText(), nullptr, checked) && !checked)
m_renderer.SetCurrentAov(aov);
}
ImGui::EndMenu();
}
}
// -- Lighting ----------------------------------------------------
if (ImGui::BeginMenu("Lighting")) {
bool ambient = m_renderer.GetAmbientLightOnly();
if (ImGui::MenuItem("Camera Headlight", nullptr, ambient))
m_renderer.SetAmbientLightOnly(true);
if (ImGui::MenuItem("Scene Lights", nullptr, !ambient))
m_renderer.SetAmbientLightOnly(false);
ImGui::Separator();
bool dome = m_renderer.GetDomeLightEnabled();
if (ImGui::MenuItem("Dome Light", nullptr, dome))
m_renderer.SetDomeLightEnabled(!dome);
ImGui::EndMenu();
}
ImGui::Separator();
// -- Display toggles ---------------------------------------------
{
bool grid = m_renderer.ShowGrid();
if (ImGui::MenuItem("Show Grid", nullptr, grid))
m_renderer.SetShowGrid(!grid);
bool guide = m_renderer.ShowCameraGuide();
if (ImGui::MenuItem("Camera Guide", nullptr, guide))
m_renderer.SetShowCameraGuide(!guide);
if (ImGui::BeginMenu("Purpose")) {
bool purGuide = m_renderer.ShowGuides();
if (ImGui::MenuItem("Guide", nullptr, purGuide))
m_renderer.SetShowGuides(!purGuide);
bool purProxy = m_renderer.ShowProxy();
if (ImGui::MenuItem("Proxy", nullptr, purProxy))
m_renderer.SetShowProxy(!purProxy);
bool purRender = m_renderer.ShowRender();
if (ImGui::MenuItem("Render", nullptr, purRender))
m_renderer.SetShowRender(!purRender);
ImGui::EndMenu();
}
bool aaItem = m_renderer.GetAAEnabled();
if (ImGui::MenuItem("Anti-Aliasing", nullptr, aaItem))
m_renderer.SetAAEnabled(!aaItem);
}
ImGui::Separator();
// -- BBox --------------------------------------------------------
if (ImGui::BeginMenu("Bounding Box")) {
BBoxMode bbox = m_renderer.GetBBoxMode();
if (ImGui::MenuItem("None", nullptr, bbox == BBoxMode::None))
m_renderer.SetBBoxMode(BBoxMode::None);
if (ImGui::MenuItem("Per Object", nullptr, bbox == BBoxMode::PerObject))
m_renderer.SetBBoxMode(BBoxMode::PerObject);
if (ImGui::MenuItem("All Selection", nullptr, bbox == BBoxMode::AllSelection))
m_renderer.SetBBoxMode(BBoxMode::AllSelection);
ImGui::EndMenu();
}
// -- Background --------------------------------------------------
if (ImGui::BeginMenu("Background")) {
auto& bg = m_renderer.GetBackgroundColor();
if (ImGui::MenuItem("Dark Gray", nullptr, bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f));
if (ImGui::MenuItem("Black", nullptr, bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
if (ImGui::MenuItem("Light Gray", nullptr, bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f));
if (ImGui::MenuItem("Midnight Blue", nullptr, bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f));
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Renderer Settings..."))
m_showRendererSettings = true;
ImGui::EndPopup();
}
}
ImGui::PopStyleVar(); // ItemSpacing ImGui::PopStyleVar(); // ItemSpacing
} }
@@ -1104,6 +1297,9 @@ void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size,
m_stage, m_selectedSdfPaths, m_stage, m_selectedSdfPaths,
m_camera.GetUsdCameraPath(), m_camera.GetUsdCameraPath(),
viewProj, m_camera.GetDist()); viewProj, m_camera.GetDist());
m_renderer.DrawLightWireframes(
m_stage, m_selectedSdfPaths,
viewProj, m_camera.GetDist());
} }
// Display FBO texture (flip UV Y: OpenGL is bottom-up) // Display FBO texture (flip UV Y: OpenGL is bottom-up)
@@ -1167,6 +1363,143 @@ void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size,
RenderContextMenu(tileIndex); RenderContextMenu(tileIndex);
ImGui::EndChild(); ImGui::EndChild();
if (m_showRendererSettings)
RenderRendererSettingsDialog(tileIndex);
}
// ---------------------------------------------------------------------------
// RenderRendererSettingsDialog
// ---------------------------------------------------------------------------
void ViewportTile::RenderRendererSettingsDialog(int tileIndex)
{
using namespace pxr;
std::string title = "Renderer Settings [" +
UsdSceneRenderer::GetRendererDisplayName(m_renderer.GetCurrentRendererId()) +
"]###RendererSettingsDlg_" + std::to_string(tileIndex);
ImGui::SetNextWindowSize(ImVec2(520, 460), ImGuiCond_FirstUseEver);
// WindowPadding is captured by Begin(); pop it immediately after.
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.f, 12.f));
bool open = ImGui::Begin(title.c_str(), &m_showRendererSettings,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar();
if (!open) {
ImGui::End();
return;
}
// ItemSpacing and FramePadding are read dynamically; push for content, pop before End.
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.f, 6.f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.f, 4.f));
// Cache the settings list per delegate — GetRendererSettingsList() calls TF_WARN
// for every unsupported setting type, so querying it every frame floods the log.
pxr::TfToken curId = m_renderer.GetCurrentRendererId();
if (curId != m_cachedSettingsRendererId) {
m_cachedRendererSettings = m_renderer.GetRendererSettingsList();
m_cachedSettingsRendererId = curId;
}
const auto& settings = m_cachedRendererSettings;
if (settings.empty()) {
ImVec2 sz = ImGui::GetContentRegionAvail();
ImGui::SetCursorPos(ImVec2(sz.x * 0.5f - 120.f, sz.y * 0.5f - 8.f));
ImGui::TextDisabled("This renderer exposes no settings.");
ImGui::PopStyleVar(2);
ImGui::End();
return;
}
// Group settings by namespace prefix (split key at first ':').
// Keys without ':' go into "General". std::map keeps alphabetical order.
std::map<std::string, std::vector<const UsdImagingGLRendererSetting*>> groups;
for (const auto& s : settings) {
std::string k = s.key.GetString();
auto colon = k.find(':');
std::string cat = (colon != std::string::npos) ? k.substr(0, colon) : "General";
if (!cat.empty()) cat[0] = static_cast<char>(toupper(static_cast<unsigned char>(cat[0])));
groups[cat].push_back(&s);
}
bool singleGroup = (groups.size() == 1);
bool tabBarOpen = false;
if (!singleGroup)
tabBarOpen = ImGui::BeginTabBar("##rdrsettings_tabs");
auto renderGroup = [&](const std::vector<const UsdImagingGLRendererSetting*>& items) {
float tableH = ImGui::GetContentRegionAvail().y - 40.f;
if (tableH < 20.f) tableH = 20.f;
if (ImGui::BeginTable("##srows", 2,
ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerH |
ImGuiTableFlags_ScrollY, ImVec2(0, tableH)))
{
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.55f);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.45f);
for (const auto* sp : items) {
const auto& s = *sp;
VtValue cur = m_renderer.GetRendererSetting(s.key);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(s.name.c_str());
ImGui::TableSetColumnIndex(1);
ImGui::PushItemWidth(-1.f);
std::string wid = "##" + s.key.GetString();
if (s.type == UsdImagingGLRendererSetting::TYPE_FLAG) {
bool v = cur.GetWithDefault<bool>(false);
if (ImGui::Checkbox(wid.c_str(), &v))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_INT) {
int v = cur.GetWithDefault<int>(0);
if (ImGui::DragInt(wid.c_str(), &v))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_FLOAT) {
float v = cur.GetWithDefault<float>(0.f);
if (ImGui::DragFloat(wid.c_str(), &v, 0.01f))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_STRING) {
std::string sv = cur.GetWithDefault<std::string>("");
char buf[256];
strncpy(buf, sv.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText(wid.c_str(), buf, sizeof(buf)) &&
ImGui::IsItemDeactivatedAfterEdit())
m_renderer.SetRendererSetting(s.key, VtValue(std::string(buf)));
}
ImGui::PopItemWidth();
}
ImGui::EndTable();
}
};
if (singleGroup) {
renderGroup(groups.begin()->second);
} else if (tabBarOpen) {
// Non-"General" tabs first (alphabetical via std::map), then "General" last
for (const auto& [cat, items] : groups) {
if (cat == "General") continue;
if (ImGui::BeginTabItem(cat.c_str())) {
renderGroup(items);
ImGui::EndTabItem();
}
}
auto genIt = groups.find("General");
if (genIt != groups.end() && ImGui::BeginTabItem("General")) {
renderGroup(genIt->second);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Reset Defaults")) {
for (const auto& s : settings)
m_renderer.SetRendererSetting(s.key, s.defValue);
}
ImGui::PopStyleVar(2);
ImGui::End();
} }
} // namespace UsdLayerManager } // namespace UsdLayerManager
+37 -1
View File
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include "../core/UsdSceneRenderer.h" #include "../core/UsdSceneRenderer.h"
#include "../core/ViewportCamera.h" #include "../core/ViewportCamera.h"
@@ -17,6 +17,31 @@
namespace UsdLayerManager { namespace UsdLayerManager {
/// Serialisable snapshot of per-tile render settings.
/// Color correction is global (AppPreferences); render delegate is per-tile.
struct ViewportTileSettings {
std::string renderDelegate; ///< empty = use AppPreferences default on first load
bool showGrid = true;
bool showCameraGuide = false;
std::string aov = "color";
bool aaEnabled = false;
float bgColorR = 0.15f;
float bgColorG = 0.15f;
float bgColorB = 0.15f;
int bboxMode = 0;
bool ambientLightOnly = true;
bool domeLightEnabled = false;
int shadingMode = 0;
// CullStyle::Nothing — matches usdview's default (viewSettingsDataModel
// cullBackfaces=False -> CULL_STYLE_NOTHING). BackUnlessDoubleSided drops
// back faces on geometry that isn't authored doubleSided, which hdEmbree
// applies to occlusion rays as well, so interiors shade as single-sided.
int cullStyle = 1; // CullStyle::Nothing
bool showGuides = false;
bool showProxy = true;
bool showRender = false;
};
/// Named orthographic view directions. /// Named orthographic view directions.
/// When m_orthoView != None the tile renders an orthographic camera locked to /// When m_orthoView != None the tile renders an orthographic camera locked to
/// that world-space direction; the free-camera orbital state (center + dist) /// that world-space direction; the free-camera orbital state (center + dist)
@@ -87,6 +112,11 @@ public:
void FrameScene(); void FrameScene();
/// Snapshot all current render settings into a portable struct.
ViewportTileSettings GetSettings() const;
/// Apply a previously-saved settings snapshot (safe to call before first Render).
void ApplySettings(const ViewportTileSettings& s);
private: private:
// ── Render sub-functions ───────────────────────────────────────────────── // ── Render sub-functions ─────────────────────────────────────────────────
pxr::GfCamera ResolveCamera(); pxr::GfCamera ResolveCamera();
@@ -162,6 +192,12 @@ private:
// ── Per-frame interaction flags ────────────────────────────────────────── // ── Per-frame interaction flags ──────────────────────────────────────────
bool m_wasClickedThisFrame = false; bool m_wasClickedThisFrame = false;
bool m_wasHoveredThisFrame = false; bool m_wasHoveredThisFrame = false;
// ── Renderer settings dialog ─────────────────────────────────────────────
bool m_showRendererSettings = false;
pxr::TfToken m_cachedSettingsRendererId;
pxr::UsdImagingGLRendererSettingsList m_cachedRendererSettings;
void RenderRendererSettingsDialog(int tileIndex);
}; };
} // namespace UsdLayerManager } // namespace UsdLayerManager
+36
View File
@@ -1,6 +1,7 @@
#include "FileDialog.h" #include "FileDialog.h"
#include "Logger.h" #include "Logger.h"
#include <commdlg.h> #include <commdlg.h>
#include <shobjidl.h>
#include <vector> #include <vector>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -56,4 +57,39 @@ std::string FileDialog::SaveFile(const char* filter, const char* title, const ch
return ""; return "";
} }
std::string FileDialog::BrowseFolder(const char* title, HWND owner) {
IFileOpenDialog* pfd = nullptr;
if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))))
return "";
DWORD opts = 0;
pfd->GetOptions(&opts);
pfd->SetOptions(opts | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
if (title) {
int n = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0);
std::vector<wchar_t> wt(n);
MultiByteToWideChar(CP_UTF8, 0, title, -1, wt.data(), n);
pfd->SetTitle(wt.data());
}
std::string result;
if (SUCCEEDED(pfd->Show(owner))) {
IShellItem* psi = nullptr;
if (SUCCEEDED(pfd->GetResult(&psi))) {
PWSTR path = nullptr;
if (SUCCEEDED(psi->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
int n = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr);
result.resize(static_cast<size_t>(n) - 1);
WideCharToMultiByte(CP_UTF8, 0, path, -1, result.data(), n, nullptr, nullptr);
CoTaskMemFree(path);
}
psi->Release();
}
}
pfd->Release();
return result;
}
} // namespace UsdLayerManager } // namespace UsdLayerManager

Some files were not shown because too many files have changed in this diff Show More