Compare commits

..

38 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
82 changed files with 22171 additions and 750 deletions
+3
View File
@@ -1,5 +1,6 @@
# Build directories # Build directories
build/ build/
build_*/
install/ install/
out/ out/
.kilocode/ .kilocode/
@@ -78,3 +79,5 @@ imgui.ini
# OCIO config LUT files — downloaded automatically at CMake configure time # OCIO config LUT files — downloaded automatically at CMake configure time
resources/OpenColorIO-Configs/ resources/OpenColorIO-Configs/
resources/hdri/
resources/vdb/
+3 -3
View File
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build ## Build
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`. **Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
OpenUSD 25.05 is expected at `third_party/OpenUSD-v25.05` (set via `CMAKE_PREFIX_PATH` in the preset). OpenUSD 25.11 is expected at `third_party/OpenUSD-v25.11` (set via `CMAKE_PREFIX_PATH` in the preset).
```powershell ```powershell
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run) # Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
@@ -20,11 +20,11 @@ cmake --build build --config Release
cmake --build build --config Release --target install cmake --build build --config Release --target install
``` ```
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON`, `HDARNOLD_ROOT`, and `HDEMBREE_USD_ROOT` for the maintainer's machine. For a minimal build without optional render delegates, configure manually: 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 ```powershell
cmake -B build -G "Visual Studio 17 2022" ` cmake -B build -G "Visual Studio 17 2022" `
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.05" ` -DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.11" `
-DIMGUI_DIR="third_party/imgui-1.92.7" ` -DIMGUI_DIR="third_party/imgui-1.92.7" `
-DWITH_CYCLES=OFF -DWITH_CYCLES=OFF
``` ```
+140 -40
View File
@@ -13,6 +13,7 @@ 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) find_package(FFmpeg REQUIRED)
@@ -62,9 +63,13 @@ set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll") if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(OPENUSD_HAS_ARNOLD TRUE) set(OPENUSD_HAS_ARNOLD TRUE)
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll") set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/ndrArnold.dll")
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold") set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/ndrArnold") # 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") if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
set(ARNOLD_AI_DLL "${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) # ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
@@ -97,6 +102,12 @@ set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree, # tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
# not in the install output — deploy it explicitly. # 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") 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) if(WITH_CYCLES)
include(ExternalProject) include(ExternalProject)
@@ -172,6 +183,11 @@ 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")
@@ -195,6 +211,7 @@ 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 FFmpeg::FFmpeg
OpenColorIO::OpenColorIO OpenColorIO::OpenColorIO
@@ -221,13 +238,20 @@ if(WIN32)
_CRT_SECURE_NO_WARNINGS _CRT_SECURE_NO_WARNINGS
) )
# OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll) # 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 add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" ${OPENCOLORIO_RUNTIME_DLL}
"$<TARGET_FILE_DIR:UsdLayerManager>" "$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying OpenColorIO runtime DLL..." 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 # FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll") file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
@@ -316,13 +340,19 @@ if(WIN32)
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NDRARNOLD_DLL}" "${NDRARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold.dll" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDARNOLD_PLUGIN_DIR}" "${HDARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${NDRARNOLD_PLUGIN_DIR}" "${NDRARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold" "$<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..." COMMENT "Copying hdArnold plugin..."
) )
if(ARNOLD_RUNTIME_DLLS) if(ARNOLD_RUNTIME_DLLS)
@@ -360,8 +390,10 @@ if(WIN32)
" endif()\n" " endif()\n"
# Exclude debug-variant DLLs (not needed in release, just wasted space). # Exclude debug-variant DLLs (not needed in release, just wasted space).
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_" # Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
# openjph.0.25d.dll → ends with "d.dll" # openjph.0.25d.dll → version digit directly followed by "d.dll"
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n" # 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" " continue()\n"
" endif()\n" " endif()\n"
" list(APPEND _dlls \"\${_dll}\")\n" " list(APPEND _dlls \"\${_dll}\")\n"
@@ -384,6 +416,10 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_TBB12_DLL}" "${CYCLES_TBB12_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/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..." COMMENT "Copying hdCycles plugin and runtime DLLs..."
) )
endif() endif()
@@ -461,6 +497,14 @@ if(OPENUSD_HAS_ARNOLD)
DESTINATION bin/usd DESTINATION bin/usd
OPTIONAL OPTIONAL
) )
install(FILES "${USDIMAGINGARNOLD_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
if(ARNOLD_RUNTIME_DLLS) if(ARNOLD_RUNTIME_DLLS)
install(FILES ${ARNOLD_RUNTIME_DLLS} install(FILES ${ARNOLD_RUNTIME_DLLS}
DESTINATION bin DESTINATION bin
@@ -504,32 +548,20 @@ if(OPENUSD_HAS_CYCLES)
DESTINATION bin DESTINATION bin
OPTIONAL OPTIONAL
) )
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
install(FILES "${CYCLES_OCIO25_DLL}"
DESTINATION bin
OPTIONAL
)
endif() endif()
# Install Python runtime DLLs required by OpenUSD and Cycles. # Install Python runtime DLLs required by OpenUSD and Cycles.
# python312.dll : linked by Cycles (hdCycles.dll → python312.dll) # python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
# python311.dll : linked by usd_python.dll (the OpenUSD Python bindings were # (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
# built against Python 3.11, regardless of what we compile with)
# Detect python311.dll from common install paths.
foreach(_pyroot
"$ENV{LOCALAPPDATA}/Programs/Python/Python311"
"C:/Python311"
"C:/Program Files/Python311"
)
if(EXISTS "${_pyroot}/python311.dll")
set(PYTHON311_DLL "${_pyroot}/python311.dll")
message(STATUS "Found python311.dll (required by usd_python.dll): ${PYTHON311_DLL}")
break()
endif()
endforeach()
set(_python_runtime_dlls 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"
) )
if(PYTHON311_DLL)
list(APPEND _python_runtime_dlls "${PYTHON311_DLL}")
endif()
install(FILES ${_python_runtime_dlls} install(FILES ${_python_runtime_dlls}
DESTINATION bin DESTINATION bin
@@ -537,17 +569,6 @@ install(FILES ${_python_runtime_dlls}
# system-wide Python; in that case the DLL is on PATH already. # system-wide Python; in that case the DLL is on PATH already.
) )
# Also copy python311.dll to the build output directory so debug runs work
# without requiring C:\Python311 to be on PATH.
if(PYTHON311_DLL)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PYTHON311_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/python311.dll"
COMMENT "Copying python311.dll (for usd_python.dll)..."
)
endif()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# OCIO: download ACES 1.2 config at configure time if not present # OCIO: download ACES 1.2 config at configure time if not present
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -577,6 +598,60 @@ if(NOT EXISTS "${_ocio_config_dir}/config.ocio")
endif() endif()
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}
@@ -592,7 +667,24 @@ 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). # Copy ACES OCIO config to build output — only on first build (skipped if already present).
@@ -608,7 +700,7 @@ endif()
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/) # Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
install(FILES install(FILES
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" ${OPENCOLORIO_RUNTIME_DLL}
DESTINATION bin DESTINATION bin
OPTIONAL OPTIONAL
) )
@@ -619,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
+4 -5
View File
@@ -9,15 +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": "OFF", "BUILD_TESTS": "OFF",
"WITH_CYCLES": "ON", "WITH_CYCLES": "ON",
"HDARNOLD_ROOT": "E:/library/hdArnold", "HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026", "ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
"HDEMBREE_USD_ROOT": "E:/USD_v25.05_embree"
} }
}, },
{ {
@@ -36,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"
} }
}, },
+40 -21
View File
@@ -1,26 +1,32 @@
# 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 (Storm, Embree, Arnold, Cycles); 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`) |
@@ -29,8 +35,8 @@ A C++17 / Windows desktop application providing a **Maya Render Layers-style** U
| Delegate | Renderer Version | Variable | Notes | | Delegate | Renderer Version | Variable | Notes |
|---|---|---|---| |---|---|---|---|
| **hdEmbree** | Embree 4.4.1 | `HDEMBREE_USD_ROOT` | USD build with `PXR_ENABLE_EMBREE_PLUGIN=ON`; set `EMBREE_LOCATION` if Embree DLLs are not in `HDEMBREE_USD_ROOT/bin` | | **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) install dir + MtoA root for `ai.dll` | | **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 | | **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`. All delegate paths are pre-configured in `CMakePresets.json`.
@@ -53,7 +59,7 @@ cmake --install build --config Release
The installed executable 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 ### Building with Cycles
@@ -63,10 +69,7 @@ Cycles runtime dependencies (`embree4.dll`, `openvdb.dll`, `OpenImageDenoise.dll
### 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
@@ -77,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)
@@ -95,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
``` ```
@@ -111,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
@@ -121,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 |
@@ -146,15 +166,14 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
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
+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
+12 -1
View File
@@ -1,6 +1,6 @@
# ADR 0001 — Custom Viewport Color Correction (sRGB / OCIO) # ADR 0001 — Custom Viewport Color Correction (sRGB / OCIO)
- **Status:** Accepted - **Status:** Superseded (2026-06-30) — replaced by the stageView.py approach (HdxColorCorrectionTask via RenderParams)
- **Date:** 2026-06-27 - **Date:** 2026-06-27
- **Component:** `src/core/UsdSceneRenderer.cpp` (`ViewportColorCorrector`, `ApplyViewportColorCorrection`) - **Component:** `src/core/UsdSceneRenderer.cpp` (`ViewportColorCorrector`, `ApplyViewportColorCorrection`)
- **Commit:** `fb1ae9d` - **Commit:** `fb1ae9d`
@@ -181,3 +181,14 @@ expected corrected (non-black) image and sRGB is unchanged. Headless automated
verification is blocked by the stale test-target CMake config (missing USD 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 include dirs); restoring those targets would let an OCIO-vs-sRGB center-pixel
test assert this automatically. 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: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

+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

+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
+122 -362
View File
@@ -28,16 +28,12 @@
#include <pxr/base/tf/warning.h> #include <pxr/base/tf/warning.h>
#include <pxr/base/tf/status.h> #include <pxr/base/tf/status.h>
#include <OpenColorIO/OpenColorIO.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <exception> #include <exception>
#include <unordered_set> #include <unordered_set>
namespace OCIO = OCIO_NAMESPACE;
namespace UsdLayerManager { namespace UsdLayerManager {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -143,271 +139,6 @@ static GLuint LinkProgram(GLuint vs, GLuint fs) {
return prog; return prog;
} }
// ===========================================================================
// ViewportColorCorrector — custom GL color correction (sRGB / OCIO)
// ===========================================================================
//
// Replaces HdxColorCorrectionTask. The scene is rendered linear; Apply()
// samples that linear texture and draws a corrected fullscreen triangle into
// the currently-bound FBO. For OCIO it uses the OCIO GPU shader API directly
// (GpuShaderDesc) so the exact transform — including 1D/3D LUTs — is built and
// bound under our control, independent of Hydra.
class ViewportColorCorrector {
public:
enum class Mode { sRGB, OpenColorIO };
~ViewportColorCorrector() { DestroyGLResources(); }
/// Draw a corrected fullscreen quad into the bound FBO sampling srcTex
/// (single-sample, linear RGBA). Caller has set the GL viewport.
/// Returns true if a pass was drawn. On OCIO build failure it falls back
/// to the sRGB encode so the viewport degrades gracefully (never black).
bool Apply(GLuint srcTex, Mode mode,
const std::string& disp, const std::string& view,
const std::string& cs, const std::string& look)
{
if (!EnsureCommon()) return false;
GLuint prog = m_srgbProgram;
std::vector<LutTex>* luts = nullptr;
if (mode == Mode::OpenColorIO && EnsureOcioProgram(disp, view, cs, look)) {
prog = m_ocioProgram;
luts = &m_ocioLuts;
}
if (!prog) return false;
// Save the GL state we touch.
GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean blend = glIsEnabled(GL_BLEND);
GLboolean depthMask = GL_TRUE;
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDepthMask(GL_FALSE);
glUseProgram(prog);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, srcTex);
glUniform1i(glGetUniformLocation(prog, "uTex"), 0);
if (luts) {
int unit = 1;
for (const auto& l : *luts) {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(l.target, l.id);
GLint loc = glGetUniformLocation(prog, l.sampler.c_str());
if (loc >= 0) glUniform1i(loc, unit);
++unit;
}
glActiveTexture(GL_TEXTURE0);
}
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
// Restore state.
if (depthTest) glEnable(GL_DEPTH_TEST);
if (blend) glEnable(GL_BLEND);
glDepthMask(depthMask);
return true;
}
void DestroyGLResources() {
DestroyOcioResources();
if (m_srgbProgram) { glDeleteProgram(m_srgbProgram); m_srgbProgram = 0; }
if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; }
}
private:
struct LutTex { GLuint id; GLenum target; std::string sampler; };
bool EnsureCommon() {
if (m_vao == 0) glGenVertexArrays(1, &m_vao);
if (m_srgbProgram == 0) {
GLuint vs = CompileShader(kFullscreenVS, GL_VERTEX_SHADER);
GLuint fs = CompileShader(kSrgbFS, GL_FRAGMENT_SHADER);
if (vs && fs) m_srgbProgram = LinkProgram(vs, fs);
}
return m_vao != 0 && m_srgbProgram != 0;
}
bool EnsureOcioProgram(const std::string& disp, const std::string& view,
const std::string& cs, const std::string& look)
{
const std::string key = disp + "|" + view + "|" + cs + "|" + look;
if (m_ocioProgram && key == m_ocioKey) return true;
if (key == m_ocioFailedKey) return false;
DestroyOcioResources();
m_ocioKey.clear();
std::string fragText;
OCIO::GpuShaderDescRcPtr desc;
try {
OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();
if (!config) throw std::runtime_error("no current OCIO config");
const char* srcCS = cs.empty() ? OCIO::ROLE_SCENE_LINEAR : cs.c_str();
OCIO::ConstProcessorRcPtr proc = config->getProcessor(
srcCS, disp.c_str(), view.c_str(), OCIO::TRANSFORM_DIR_FORWARD);
OCIO::ConstGPUProcessorRcPtr gpu = proc->getDefaultGPUProcessor();
desc = OCIO::GpuShaderDesc::CreateShaderDesc();
desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_1_3);
desc->setFunctionName("OCIODisplay");
desc->setResourcePrefix("ocio_");
gpu->extractGpuShaderInfo(desc);
fragText = desc->getShaderText();
} catch (const std::exception& e) {
LOG_WARNING("Custom OCIO build failed (" + key
+ "), using sRGB: " + std::string(e.what()));
m_ocioFailedKey = key;
return false;
}
// Assemble the fragment shader: OCIO declares its samplers + the
// OCIODisplay(vec4) function; our main() samples the linear input and
// runs it through.
std::string fs = "#version 130\n"
"uniform sampler2D uTex;\n"
"in vec2 vUv;\n"
"out vec4 outColor;\n"
+ fragText +
"\nvoid main(){ outColor = OCIODisplay(texture(uTex, vUv)); }\n";
GLuint vsh = CompileShader(kFullscreenVS, GL_VERTEX_SHADER);
GLuint fsh = CompileShader(fs.c_str(), GL_FRAGMENT_SHADER);
GLuint prog = (vsh && fsh) ? LinkProgram(vsh, fsh) : 0;
if (!prog) {
LOG_WARNING("Custom OCIO shader compile/link failed for " + key
+ " — using sRGB");
m_ocioFailedKey = key;
return false;
}
// Upload the LUT textures OCIO requested.
if (!CreateOcioTextures(desc)) {
glDeleteProgram(prog);
DestroyOcioResources();
m_ocioFailedKey = key;
return false;
}
m_ocioProgram = prog;
m_ocioKey = key;
LOG_INFO("Custom OCIO program built: " + key
+ " (" + std::to_string(m_ocioLuts.size()) + " LUTs)");
return true;
}
bool CreateOcioTextures(const OCIO::GpuShaderDescRcPtr& desc) {
// 3D LUTs (RGB).
for (unsigned i = 0; i < desc->getNum3DTextures(); ++i) {
const char* texName = nullptr; const char* samplerName = nullptr;
unsigned edgelen = 0; OCIO::Interpolation interp = OCIO::INTERP_LINEAR;
desc->get3DTexture(i, texName, samplerName, edgelen, interp);
const float* values = nullptr;
desc->get3DTextureValues(i, values);
if (!values || edgelen == 0 || !samplerName) return false;
GLuint id = 0;
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_3D, id);
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB32F, edgelen, edgelen, edgelen,
0, GL_RGB, GL_FLOAT, values);
GLint filt = (interp == OCIO::INTERP_NEAREST) ? GL_NEAREST : GL_LINEAR;
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, filt);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, filt);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
m_ocioLuts.push_back({ id, GL_TEXTURE_3D, samplerName });
}
// 1D / 2D LUTs.
for (unsigned i = 0; i < desc->getNumTextures(); ++i) {
const char* texName = nullptr; const char* samplerName = nullptr;
unsigned width = 0, height = 0;
OCIO::GpuShaderDesc::TextureType channel = OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL;
OCIO::Interpolation interp = OCIO::INTERP_LINEAR;
desc->getTexture(i, texName, samplerName, width, height, channel, interp);
const float* values = nullptr;
desc->getTextureValues(i, values);
if (!values || width == 0 || !samplerName) return false;
const bool isRed = (channel == OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL);
const GLint internal = isRed ? GL_R32F : GL_RGB32F;
const GLenum format = isRed ? GL_RED : GL_RGB;
const GLint filt = (interp == OCIO::INTERP_NEAREST) ? GL_NEAREST : GL_LINEAR;
const GLenum target = (height > 1) ? GL_TEXTURE_2D : GL_TEXTURE_1D;
GLuint id = 0;
glGenTextures(1, &id);
glBindTexture(target, id);
if (target == GL_TEXTURE_2D) {
glTexImage2D(GL_TEXTURE_2D, 0, internal, width, height, 0,
format, GL_FLOAT, values);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
} else {
glTexImage1D(GL_TEXTURE_1D, 0, internal, width, 0,
format, GL_FLOAT, values);
}
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filt);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filt);
glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
m_ocioLuts.push_back({ id, target, samplerName });
}
return true;
}
void DestroyOcioResources() {
for (auto& l : m_ocioLuts) glDeleteTextures(1, &l.id);
m_ocioLuts.clear();
if (m_ocioProgram) { glDeleteProgram(m_ocioProgram); m_ocioProgram = 0; }
m_ocioKey.clear();
m_ocioFailedKey.clear();
}
static const char* kFullscreenVS;
static const char* kSrgbFS;
GLuint m_vao = 0;
GLuint m_srgbProgram = 0;
GLuint m_ocioProgram = 0;
std::string m_ocioKey;
std::string m_ocioFailedKey;
std::vector<LutTex> m_ocioLuts;
};
// Attribute-less fullscreen triangle; UV in [0,2] covers the [0,1] screen.
const char* ViewportColorCorrector::kFullscreenVS = R"(#version 130
out vec2 vUv;
void main() {
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
vUv = p;
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}
)";
// Linear → sRGB encode (matches HdxColorCorrectionTask's sRGB path).
const char* ViewportColorCorrector::kSrgbFS = R"(#version 130
uniform sampler2D uTex;
in vec2 vUv;
out vec4 outColor;
vec3 lin2srgb(vec3 c) {
vec3 lo = c * 12.92;
vec3 hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0/2.4)) - 0.055;
bvec3 cut = lessThanEqual(c, vec3(0.0031308));
return mix(hi, lo, vec3(cut));
}
void main() {
vec4 c = texture(uTex, vUv);
outColor = vec4(lin2srgb(c.rgb), c.a);
}
)";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// GLSL sources // GLSL sources
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -435,9 +166,15 @@ 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_shadingMode(ShadingMode::SmoothShaded)
, m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
, m_colorCorrectionMode(ColorCorrectionMode::sRGB) , m_colorCorrectionMode(ColorCorrectionMode::sRGB)
, m_ambientLightOnly(true) , m_ambientLightOnly(true)
, m_domeLightEnabled(false) , m_domeLightEnabled(false)
@@ -478,9 +215,6 @@ UsdSceneRenderer::~UsdSceneRenderer() {
DestroyBBoxResources(); DestroyBBoxResources();
DestroyCamWireResources(); DestroyCamWireResources();
DestroyLightWireResources(); DestroyLightWireResources();
m_colorCorrector.reset(); // deletes its GL program / LUT textures
if (m_ccLinearFBO) glDeleteFramebuffers(1, &m_ccLinearFBO);
if (m_ccLinearTex) glDeleteTextures(1, &m_ccLinearTex);
} }
// =========================================================================== // ===========================================================================
@@ -547,26 +281,28 @@ void UsdSceneRenderer::InitRenderer() {
if (!hasCycles) if (!hasCycles)
LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors)."); 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));
@@ -611,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()));
@@ -620,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
// =========================================================================== // ===========================================================================
@@ -835,9 +605,8 @@ 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();
// RGBA16F so the scene is stored linear with HDR headroom: we render // RGBA16F: Hydra's internal render buffers carry linear HDR values;
// linear (Hydra correction disabled) and apply our own color correction // RGBA16F on the draw target preserves precision in the corrected output.
// afterwards, which needs values outside [0,1] for OCIO/ACES.
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F); 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);
@@ -936,7 +705,7 @@ 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 // Apply shading mode → drawMode + enableLighting
switch (m_shadingMode) { switch (m_shadingMode) {
case ShadingMode::FlatShaded: case ShadingMode::FlatShaded:
@@ -955,12 +724,12 @@ void UsdSceneRenderer::Render(int width, int height)
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH; m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
m_renderParams.enableLighting = true; break; m_renderParams.enableLighting = true; break;
} }
m_renderParams.showGuides = true; m_renderParams.showGuides = m_showGuides;
m_renderParams.showProxy = true; m_renderParams.showProxy = m_showProxy;
m_renderParams.showRender = false; 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;
@@ -969,11 +738,28 @@ 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 is done by our own GL post-process (ApplyViewport- // Color correction — stageView.py approach: pass the actual mode + OCIO
// ColorCorrection below), not HdxColorCorrectionTask — so Hydra always // params to HdxColorCorrectionTask via RenderParams and SetColorCorrection-
// renders linear ("disabled"). This bypasses the hdx OCIO path entirely. // Settings (mirrors stageView.renderSinglePass).
m_renderParams.colorCorrectionMode = pxr::TfToken("disabled"); pxr::TfToken ccToken("disabled");
m_renderer->SetColorCorrectionSettings(pxr::TfToken("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 // Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below
// would be skipped, permanently unbalancing the GlfDrawTarget bind stack // would be skipped, permanently unbalancing the GlfDrawTarget bind stack
@@ -985,11 +771,6 @@ void UsdSceneRenderer::Render(int width, int height)
} }
m_forceRefresh = false; m_forceRefresh = false;
// --- Custom color-correction post-process (linear → sRGB / OCIO) ---
if (m_colorCorrectionMode != ColorCorrectionMode::Disabled) {
ApplyViewportColorCorrection(width, height);
}
// --- Optional grid overlay --- // --- Optional grid overlay ---
if (m_showGrid) { if (m_showGrid) {
// When MSAA is active, render the grid into the MSAA FBO so it is // When MSAA is active, render the grid into the MSAA FBO so it is
@@ -1018,66 +799,6 @@ void UsdSceneRenderer::Render(int width, int height)
m_drawTarget->Unbind(); m_drawTarget->Unbind();
} }
// ===========================================================================
// Custom color-correction post-process
// ===========================================================================
void UsdSceneRenderer::ApplyViewportColorCorrection(int width, int height)
{
if (!m_drawTarget) return;
if (!m_colorCorrector)
m_colorCorrector = std::make_unique<ViewportColorCorrector>();
// (Re)create the single-sample linear copy texture + its FBO on size change.
if (m_ccLinearTex == 0 || m_ccLinearW != width || m_ccLinearH != height) {
if (m_ccLinearTex == 0) glGenTextures(1, &m_ccLinearTex);
glBindTexture(GL_TEXTURE_2D, m_ccLinearTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0,
GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_ccLinearFBO == 0) glGenFramebuffers(1, &m_ccLinearFBO);
glBindFramebuffer(GL_FRAMEBUFFER, m_ccLinearFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, m_ccLinearTex, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
m_ccLinearW = width;
m_ccLinearH = height;
}
// Resolve MSAA (no-op otherwise) so the color attachment holds the linear
// single-sample image, then copy it into m_ccLinearTex — sampling and
// writing the same texture in one pass is illegal, hence the copy.
m_drawTarget->Resolve();
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_ccLinearFBO);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
// Draw the corrected result back into the draw-target render FBO (the MSAA
// FBO when multisampling, so it resolves together with the overlays drawn
// on top of it afterwards).
GLuint dstFbo = m_drawTarget->HasMSAA()
? m_drawTarget->GetFramebufferMSId()
: m_drawTarget->GetFramebufferId();
glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
glViewport(0, 0, width, height);
ViewportColorCorrector::Mode mode =
(m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO)
? ViewportColorCorrector::Mode::OpenColorIO
: ViewportColorCorrector::Mode::sRGB;
m_colorCorrector->Apply(m_ccLinearTex, mode,
m_ocioDisplay, m_ocioView,
m_ocioColorSpace, m_ocioLook);
}
// =========================================================================== // ===========================================================================
// Output // Output
// =========================================================================== // ===========================================================================
@@ -1783,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
// =========================================================================== // ===========================================================================
@@ -1837,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;
@@ -1872,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
+53 -17
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>
@@ -25,10 +27,6 @@
namespace UsdLayerManager { namespace UsdLayerManager {
/// Custom viewport color-correction post-process (sRGB / OCIO) implemented in
/// our own GL shader instead of HdxColorCorrectionTask. Defined in the .cpp.
class ViewportColorCorrector;
/// Bounding box display mode for selected prims. /// Bounding box display mode for selected prims.
enum class BBoxMode { enum class BBoxMode {
None, ///< No bounding boxes drawn None, ///< No bounding boxes drawn
@@ -45,6 +43,15 @@ enum class ShadingMode {
Unlit, ///< Smooth geometry, lighting disabled 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. /// Viewport color correction mode — maps to UsdImagingGLRenderParams::colorCorrectionMode.
enum class ColorCorrectionMode { enum class ColorCorrectionMode {
Disabled, ///< No correction (raw linear output) Disabled, ///< No correction (raw linear output)
@@ -178,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; }
@@ -223,6 +260,9 @@ public:
ShadingMode GetShadingMode() const { return m_shadingMode; } ShadingMode GetShadingMode() const { return m_shadingMode; }
void SetShadingMode(ShadingMode m) { m_shadingMode = m; m_forceRefresh = true; } 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; } ColorCorrectionMode GetColorCorrectionMode() const { return m_colorCorrectionMode; }
void SetColorCorrectionMode(ColorCorrectionMode m) { m_colorCorrectionMode = m; m_forceRefresh = true; } void SetColorCorrectionMode(ColorCorrectionMode m) { m_colorCorrectionMode = m; m_forceRefresh = true; }
@@ -313,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;
@@ -331,11 +371,18 @@ 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 ShadingMode m_shadingMode; // draw mode + lighting flags
CullStyle m_cullStyle; // back/front/none face culling
ColorCorrectionMode m_colorCorrectionMode; // viewport color correction ColorCorrectionMode m_colorCorrectionMode; // viewport color correction
std::string m_ocioDisplay; std::string m_ocioDisplay;
std::string m_ocioView; std::string m_ocioView;
@@ -392,17 +439,6 @@ private:
pxr::SdfPathVector m_cachedCameraPaths; pxr::SdfPathVector m_cachedCameraPaths;
bool m_cameraCacheDirty = true; bool m_cameraCacheDirty = true;
// --- Custom color-correction post-process ---------------------------------
// The scene is rendered linear (Hydra correction "disabled") and corrected
// by our own GL shader, bypassing HdxColorCorrectionTask. ApplyViewport-
// ColorCorrection() resolves the linear result into m_ccLinearTex, then
// draws a corrected fullscreen quad back into the draw-target FBO.
void ApplyViewportColorCorrection(int width, int height);
std::unique_ptr<ViewportColorCorrector> m_colorCorrector;
GLuint m_ccLinearFBO = 0; ///< FBO wrapping m_ccLinearTex (correction input)
GLuint m_ccLinearTex = 0; ///< single-sample linear copy of the Hydra output
int m_ccLinearW = 0;
int m_ccLinearH = 0;
}; };
} // 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
+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
+49
View File
@@ -5,6 +5,7 @@
#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];
@@ -18,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 + "\\*";
@@ -98,6 +138,15 @@ static void SetUsdPluginPath() {
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 ===");
+459 -1
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,6 +11,7 @@
#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>
@@ -39,11 +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_showCurveEditor(false)
, m_showMaterialEditor(false)
, m_running(false) { , m_running(false) {
} }
@@ -64,15 +68,23 @@ 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>();
@@ -82,6 +94,12 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_curveEditorPanel = std::make_unique<CurveEditorPanel>(); m_curveEditorPanel = std::make_unique<CurveEditorPanel>();
m_curveEditorPanel->SetCommandHistory(&m_commandHistory); 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) {
@@ -102,15 +120,24 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
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_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(
@@ -123,6 +150,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_sceneHierarchyPanel->SetSelectedPath(path); m_sceneHierarchyPanel->SetSelectedPath(path);
m_propertyPanel->SetSelectedPrimPath(path); m_propertyPanel->SetSelectedPrimPath(path);
m_curveEditorPanel->SetSelectedPrimPath(path); m_curveEditorPanel->SetSelectedPrimPath(path);
m_materialEditorPanel->SetTargetPrimPath(path);
}; };
// Rect drag in viewport → sync hierarchy + property panel + curve editor (primary) // Rect drag in viewport → sync hierarchy + property panel + curve editor (primary)
@@ -130,8 +158,17 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
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_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 {
@@ -150,13 +187,37 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
} }
} }
// Load per-viewport settings (render delegate, grid, AA, etc.) from AppData. // 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")) { if (const char* appData = getenv("APPDATA")) {
namespace fs = std::filesystem; namespace fs = std::filesystem;
fs::path dir = fs::path(appData) / "UsdLayerManager"; fs::path dir = fs::path(appData) / "UsdLayerManager";
fs::create_directories(dir); fs::create_directories(dir);
m_viewportSettingsPath = (dir / "viewport_settings.ini").string(); m_viewportSettingsPath = (dir / "viewport_settings.ini").string();
m_prefsPath = (dir / "preferences.ini").string();
m_viewportPanel->LoadSettings(m_viewportSettingsPath); 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");
@@ -178,12 +239,19 @@ void Application::Run() {
void Application::Shutdown() { void Application::Shutdown() {
if (m_viewportPanel && !m_viewportSettingsPath.empty()) if (m_viewportPanel && !m_viewportSettingsPath.empty())
m_viewportPanel->SaveSettings(m_viewportSettingsPath); 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) {
@@ -208,21 +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_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_curveEditorPanel->SetStage(nullptr);
m_materialManager->SetStage(nullptr);
m_materialEditorPanel->SetStage(nullptr);
} }
} }
@@ -268,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);
} }
@@ -300,6 +388,16 @@ void Application::RenderUI() {
ImGui::End(); 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 // Capture happens after the timeline so that the "Start" button click in
// this frame is detected, and after the viewport has rendered the scene. // this frame is detected, and after the viewport has rendered the scene.
if (m_timelinePanel->IsPlayblasting()) { if (m_timelinePanel->IsPlayblasting()) {
@@ -346,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")) {
@@ -403,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();
} }
} }
@@ -436,11 +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("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);
@@ -530,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() {
@@ -549,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
@@ -558,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.
+58 -5
View File
@@ -4,13 +4,17 @@
#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 "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 "../utils/MovieEncoder.h"
#include <memory> #include <memory>
@@ -18,6 +22,23 @@
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();
@@ -37,42 +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);
// Playblast — called each frame while a capture is in progress. /// 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(); 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<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_showCurveEditor;
bool m_showMaterialEditor;
bool m_running; bool m_running;
MovieEncoder m_movieEncoder; MovieEncoder m_movieEncoder;
std::string m_viewportSettingsPath; ///< %APPDATA%\UsdLayerManager\viewport_settings.ini 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
+3
View File
@@ -61,6 +61,8 @@ static const char* IconFilename(Icon icon) {
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::Settings: return "gear.svg";
case Icon::ChevronUp: return "chevron-up.svg";
case Icon::ChevronDown: return "chevron-down.svg";
default: return nullptr; default: return nullptr;
} }
} }
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
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::Settings, Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
Icon::ChevronUp, Icon::ChevronDown,
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+3
View File
@@ -55,6 +55,9 @@ enum class Icon {
FilePlus, // create new file FilePlus, // create new file
Pen, // edit / edit target Pen, // edit / edit target
Settings, // gear / viewport options 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
+14
View File
@@ -94,6 +94,20 @@ 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. // Store imgui.ini in %APPDATA%\UsdLayerManager\ so it doesn't litter the CWD.
if (const char* appData = getenv("APPDATA")) { if (const char* appData = getenv("APPDATA")) {
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
+564 -37
View File
@@ -1,6 +1,15 @@
#include "PropertyPanel.h" #include "PropertyPanel.h"
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../core/commands/TransformCommand.h" #include "../core/commands/TransformCommand.h"
#include "../core/commands/AttributeSetCommand.h"
#include <pxr/usd/sdf/listOp.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/payload.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/attribute.h> #include <pxr/usd/usd/attribute.h>
@@ -38,8 +47,15 @@
#include <pxr/base/vt/array.h> #include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h> #include <pxr/base/tf/token.h>
#include <pxr/usd/sdf/assetPath.h> #include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdShade/tokens.h>
#include <pxr/usd/usd/schemaRegistry.h>
#include <pxr/usd/usd/primDefinition.h>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <unordered_map>
#include <vector>
#include <set>
PXR_NAMESPACE_USING_DIRECTIVE PXR_NAMESPACE_USING_DIRECTIVE
@@ -51,7 +67,9 @@ namespace UsdLayerManager {
static const ImVec4 kColorX { 0.86f, 0.30f, 0.30f, 1.f }; // X axis red static const ImVec4 kColorX { 0.86f, 0.30f, 0.30f, 1.f }; // X axis red
static const ImVec4 kColorY { 0.40f, 0.80f, 0.30f, 1.f }; // Y axis green static const ImVec4 kColorY { 0.40f, 0.80f, 0.30f, 1.f }; // Y axis green
static const ImVec4 kColorZ { 0.28f, 0.56f, 0.96f, 1.f }; // Z axis blue static const ImVec4 kColorZ { 0.28f, 0.56f, 0.96f, 1.f }; // Z axis blue
static const ImVec4 kColorLabel{ 0.80f, 0.80f, 0.80f, 1.f }; // row labels static const ImVec4 kColorLabel { 0.80f, 0.80f, 0.80f, 1.f }; // row labels
static const ImVec4 kColorKeyOn { 1.00f, 0.65f, 0.10f, 1.f }; // key exists at editTime
static const ImVec4 kColorKeyOff { 0.35f, 0.35f, 0.35f, 1.f }; // varying, no sample here
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
PropertyPanel::PropertyPanel() = default; PropertyPanel::PropertyPanel() = default;
@@ -535,6 +553,60 @@ static std::string GetPropDisplayName(const pxr::UsdProperty& prop) {
return ns.empty() ? bn : (ns + ":" + bn); return ns.empty() ? bn : (ns + ":" + bn);
} }
// ---------------------------------------------------------------------------
// Helper: check whether an attribute has an exact time sample at t
// ---------------------------------------------------------------------------
static bool HasExactTimeSample(const pxr::UsdAttribute& attr, double t) {
double lo = 0.0, hi = 0.0;
bool has = false;
attr.GetBracketingTimeSamples(t, &lo, &hi, &has);
return has && (lo == t) && (hi == t);
}
// ---------------------------------------------------------------------------
// Helper: small "(k)" key button for animatable (Varying) attributes.
// Orange = exact time sample at editTime; dim gray = no sample at this frame.
// Click = Set(currentValue, editTime) with undo.
// ---------------------------------------------------------------------------
static void DrawKeyButton(pxr::UsdAttribute attr,
pxr::UsdTimeCode editTime,
UsdLayerManager::CommandHistory* cmdHistory)
{
using namespace pxr;
double t = editTime.GetValue();
bool exactSample = HasExactTimeSample(attr, t);
ImVec4 col = exactSample ? kColorKeyOn : kColorKeyOff;
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
bool clicked = ImGui::SmallButton("(k)");
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Set key at frame %.3g", t);
if (!clicked || !cmdHistory) return;
VtValue val;
if (!attr.Get(&val, editTime)) return;
std::function<void()> executeFn = [attr, val, editTime]() mutable {
attr.Set(val, editTime);
};
std::function<void()> undoFn;
if (exactSample) {
// overwrite: undo restores the same value that was there before
VtValue prev;
attr.Get(&prev, editTime);
undoFn = [attr, prev, editTime]() mutable { attr.Set(prev, editTime); };
} else {
undoFn = [attr, editTime]() mutable { attr.ClearAtTime(editTime.GetValue()); };
}
cmdHistory->Push(std::make_unique<AttributeSetCommand>(
"Set Key", std::move(executeFn), std::move(undoFn)));
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper: mini-button (SmallButton coloured by authored state, like usdtweak) // Helper: mini-button (SmallButton coloured by authored state, like usdtweak)
// Opens a popup context with "Copy path" on left-click. // Opens a popup context with "Copy path" on left-click.
@@ -1052,6 +1124,186 @@ void PropertyPanel::RenderPrimHeader(const pxr::UsdPrim& prim) {
ImGui::EndTable(); ImGui::EndTable();
} }
// ---------------------------------------------------------------------------
// References & Payloads one row per list-op entry; clicking the asset path
// browses for a replacement file (undoable whole-list-op swap).
// ---------------------------------------------------------------------------
namespace {
pxr::SdfReference WithAssetPath(const pxr::SdfReference& r, const std::string& assetPath) {
return pxr::SdfReference(assetPath, r.GetPrimPath(), r.GetLayerOffset(), r.GetCustomData());
}
pxr::SdfPayload WithAssetPath(const pxr::SdfPayload& p, const std::string& assetPath) {
return pxr::SdfPayload(assetPath, p.GetPrimPath(), p.GetLayerOffset());
}
// Shared row renderer for SdfReferenceListOp / SdfPayloadListOp: one row per
// item in each list-op bucket, labelled with its op. On replace, the whole
// modified list op is authored to the edit target as one undoable command.
template <typename ListOpT>
void RenderArcListRows(const pxr::UsdPrim& prim, CommandHistory* history,
IconManager* icons,
const char* tag, const char* displayKind,
const pxr::TfToken& fieldKey, const ListOpT& listOp, int& uid)
{
using ItemVector = typename ListOpT::ItemVector;
struct Bucket { const char* opName; ItemVector items; };
Bucket buckets[] = {
{"explicit", listOp.GetExplicitItems()},
{"add", listOp.GetAddedItems()},
{"prepend", listOp.GetPrependedItems()},
{"append", listOp.GetAppendedItems()},
{"delete", listOp.GetDeletedItems()},
};
const float rowH = ImGui::GetFrameHeight();
for (size_t b = 0; b < sizeof(buckets) / sizeof(buckets[0]); ++b) {
for (size_t i = 0; i < buckets[b].items.size(); ++i) {
const auto& item = buckets[b].items[i];
std::string display = item.GetAssetPath();
if (!item.GetPrimPath().IsEmpty())
display += " <" + item.GetPrimPath().GetString() + ">";
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
ImGui::PushID(uid++);
ImGui::TableSetColumnIndex(0);
DrawPropertyMiniButton(tag, /*authored=*/true,
item.GetAssetPath().c_str(), displayKind);
ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("%s", buckets[b].opName);
ImGui::TableSetColumnIndex(2);
// Browse icon button first, then the path as plain text.
bool clicked;
if (icons) {
clicked = ImGui::ImageButton("##browseArc",
ImTextureRef(icons->Get(Icon::FolderOpen)),
ImVec2(14.f, 14.f));
} else {
clicked = ImGui::SmallButton("...");
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Browse for a replacement file");
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(display.empty() ? "(no asset)" : display.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s (%s)\n%s", displayKind, buckets[b].opName,
item.GetAssetPath().c_str());
if (clicked) {
// Anything USD can compose is a valid arc target: native USD
// layers plus file formats with layer plugins (MaterialX,
// Alembic).
static const char* kArcFileFilter =
"Referenceable (*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc)\0"
"*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc\0"
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0"
"MaterialX (*.mtlx)\0*.mtlx\0"
"Alembic (*.abc)\0*.abc\0"
"All Files (*.*)\0*.*\0";
std::string picked = FileDialog::OpenFile(kArcFileFilter, "Select Reference File");
if (!picked.empty() && picked != item.GetAssetPath()) {
ListOpT newOp = listOp;
ItemVector items = buckets[b].items;
items[i] = WithAssetPath(item, picked);
switch (b) {
case 0: newOp.SetExplicitItems(items); break;
case 1: newOp.SetAddedItems(items); break;
case 2: newOp.SetPrependedItems(items); break;
case 3: newOp.SetAppendedItems(items); break;
case 4: newOp.SetDeletedItems(items); break;
}
// Authors the whole (possibly multi-layer-composed) list
// op into the edit target — the pragmatic single-layer
// behaviour; undo restores the previous composed op.
if (history) {
pxr::UsdPrim p = prim;
ListOpT oldOp = listOp;
history->Push(std::make_unique<AttributeSetCommand>(
std::string("Replace ") + displayKind + " Path",
[p, fieldKey, newOp]() mutable { p.SetMetadata(fieldKey, newOp); },
[p, fieldKey, oldOp]() mutable { p.SetMetadata(fieldKey, oldOp); }));
} else {
pxr::UsdPrim(prim).SetMetadata(fieldKey, newOp);
}
}
}
ImGui::PopID();
}
}
}
} // namespace
void PropertyPanel::RenderCompositionArcsSection(const pxr::UsdPrim& prim) {
if (!ImGui::CollapsingHeader("References & Payloads", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##compArcs", 3, kFlags)) return;
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f);
ImGui::TableSetupColumn("##op", ImGuiTableColumnFlags_WidthFixed, 60.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
int uid = 0;
if (prim.HasAuthoredReferences()) {
pxr::SdfReferenceListOp refOp;
if (prim.GetMetadata(pxr::SdfFieldKeys->References, &refOp))
RenderArcListRows(prim, m_commandHistory, m_iconManager, "(R)", "Reference",
pxr::SdfFieldKeys->References, refOp, uid);
}
if (prim.HasAuthoredPayloads()) {
pxr::SdfPayloadListOp payloadOp;
if (prim.GetMetadata(pxr::SdfFieldKeys->Payload, &payloadOp))
RenderArcListRows(prim, m_commandHistory, m_iconManager, "(P)", "Payload",
pxr::SdfFieldKeys->Payload, payloadOp, uid);
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Asset Info read-only view of the composed assetInfo dictionary
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAssetInfoSection(const pxr::UsdPrim& prim) {
if (!ImGui::CollapsingHeader("Asset Info", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##assetInfo", 2, kFlags)) return;
ImGui::TableSetupColumn("##key", ImGuiTableColumnFlags_WidthFixed, 110.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
const pxr::VtDictionary info = prim.GetAssetInfo();
std::vector<std::string> keys;
keys.reserve(info.size());
for (const auto& entry : info)
keys.push_back(entry.first);
std::sort(keys.begin(), keys.end());
for (const std::string& key : keys) {
const pxr::VtValue& value = info.find(key)->second;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("%s", key.c_str());
ImGui::TableSetColumnIndex(1);
if (value.IsHolding<pxr::SdfAssetPath>()) {
const auto& assetPath = value.UncheckedGet<pxr::SdfAssetPath>();
ImGui::TextUnformatted(assetPath.GetAssetPath().c_str());
if (ImGui::IsItemHovered() && !assetPath.GetResolvedPath().empty())
ImGui::SetTooltip("resolved: %s", assetPath.GetResolvedPath().c_str());
} else {
const std::string text = pxr::TfStringify(value);
ImGui::TextUnformatted(text.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", text.c_str());
}
}
ImGui::EndTable();
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Variant Sets no borders, no header (mirrors DrawVariantSetsCombos in usdtweak) // Variant Sets no borders, no header (mirrors DrawVariantSetsCombos in usdtweak)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1109,35 +1361,117 @@ void PropertyPanel::RenderVariantSetsSection(const pxr::UsdPrim& prim) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Unified properties table (attributes + relationships) // Material binding section
//
// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties:
// • Flags: SizingFixedFit | RowBg — NO borders, NO header row
// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state
// • Col 1 (fixed 140px) : property display name (namespace:basename)
// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { void PropertyPanel::RenderMaterialBindSection(const pxr::UsdPrim& prim) {
// Exact flags used by usdtweak no borders, alternating row background using namespace pxr;
constexpr ImGuiTableFlags kFlags =
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##properties", 3, kFlags)) return; if (!prim.HasAPI<UsdShadeMaterialBindingAPI>()) return;
UsdShadeMaterialBindingAPI bindAPI(prim);
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f); if (!ImGui::CollapsingHeader("Material Binding", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##matbind", 2, kFlags)) return;
ImGui::TableSetupColumn("##mf", ImGuiTableColumnFlags_WidthFixed, 80.f);
ImGui::TableSetupColumn("##mv", ImGuiTableColumnFlags_WidthStretch);
// Direct binding (allPurpose)
{
auto direct = bindAPI.GetDirectBinding();
SdfPath matPath = direct.GetMaterialPath();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Direct");
ImGui::TableSetColumnIndex(1);
if (!matPath.IsEmpty()) {
bool authored = direct.GetBindingRel() && direct.GetBindingRel().IsAuthored();
ImVec4 col = authored ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.f);
ImGui::TextColored(col, "%s", matPath.GetText());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", matPath.GetText());
} else {
ImGui::TextDisabled("(none)");
}
}
// Preview purpose direct binding (only show if different from allPurpose)
{
auto preview = bindAPI.GetDirectBinding(UsdShadeTokens->preview);
SdfPath previewPath = preview.GetMaterialPath();
if (!previewPath.IsEmpty()) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Preview");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", previewPath.GetText());
}
}
// Full purpose direct binding
{
auto full = bindAPI.GetDirectBinding(UsdShadeTokens->full);
SdfPath fullPath = full.GetMaterialPath();
if (!fullPath.IsEmpty()) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Full");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", fullPath.GetText());
}
}
// Resolved (computed) bound material
{
UsdShadeMaterial resolved = bindAPI.ComputeBoundMaterial();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Resolved");
ImGui::TableSetColumnIndex(1);
if (resolved) {
std::string matPathStr = resolved.GetPrim().GetPath().GetString();
ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f), "%s", matPathStr.c_str());
if (OnEditMaterialRequested) {
ImGui::SameLine();
if (ImGui::SmallButton("Edit Material"))
OnEditMaterialRequested(matPathStr);
}
} else {
ImGui::TextDisabled("(none)");
}
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Inner attribute + relationship table — one category group
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAttrRelSubTable(
const std::vector<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& rels,
const pxr::UsdEditTarget& editTarget)
{
using namespace pxr;
if (attrs.empty() && rels.empty()) return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##propSubTable", 3, kFlags)) return;
// Col 0 wider when the key button column is active
float col0W = m_editTime.IsDefault() ? 20.f : 44.f;
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, col0W);
ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 140.f); ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 140.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
// No TableHeadersRow matches usdtweak "no header" intent
const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
const float rowH = ImGui::GetFrameHeight(); const float rowH = ImGui::GetFrameHeight();
int uid = 0; int uid = 0;
// ---- Attributes (mirrors usdtweak attribute loop) ---- // ── Attributes ──────────────────────────────────────────────────────────
for (auto& attr : prim.GetAttributes()) { for (auto attr : attrs) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(a)" // Col 0 (a) mini-button + optional (k) key button
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
ImGui::PushID(uid++); ImGui::PushID(uid++);
DrawPropertyMiniButton("(a)", DrawPropertyMiniButton("(a)",
@@ -1146,29 +1480,75 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
attr.GetTypeName().GetAsToken().GetText()); attr.GetTypeName().GetAsToken().GetText());
ImGui::PopID(); ImGui::PopID();
// Col 1 display name (namespace:basename) bool isVarying = (attr.GetVariability() == SdfVariabilityVarying);
if (!m_editTime.IsDefault() && isVarying) {
ImGui::SameLine(0, 2);
ImGui::PushID(uid++);
DrawKeyButton(attr, m_editTime, m_commandHistory);
ImGui::PopID();
}
// Col 1 display name + right-click context menu
ImGui::TableSetColumnIndex(1); ImGui::TableSetColumnIndex(1);
ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()) ^ 0x100);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(GetPropDisplayName(attr).c_str()); ImGui::TextUnformatted(GetPropDisplayName(attr).c_str());
if (ImGui::BeginPopupContextItem("##attrCtx")) {
if (ImGui::MenuItem("Copy attribute path"))
ImGui::SetClipboardText(attr.GetPath().GetString().c_str());
ImGui::Separator();
bool authored = attr.IsAuthoredAt(editTarget);
ImGui::BeginDisabled(!authored);
if (ImGui::MenuItem("Clear Override")) {
// Capture current authored opinions so undo can restore them
std::vector<double> times;
attr.GetTimeSamples(&times);
std::vector<std::pair<double, VtValue>> samples;
for (double t : times) {
VtValue v;
if (attr.Get(&v, UsdTimeCode(t)))
samples.emplace_back(t, v);
}
VtValue defVal;
bool hasDefault = attr.Get(&defVal, UsdTimeCode::Default());
// Col 2 value widget with PushItemWidth(-FLT_MIN) (label hidden) if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Clear Override",
[attr]() mutable { attr.Clear(); },
[attr, samps = samples, hasDefault, defVal]() mutable {
if (hasDefault) attr.Set(defVal, UsdTimeCode::Default());
for (auto& [t, v] : samps)
attr.Set(v, UsdTimeCode(t));
}
));
} else {
attr.Clear();
}
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
ImGui::PopID();
// Col 2 value widget
ImGui::TableSetColumnIndex(2); ImGui::TableSetColumnIndex(2);
ImGui::PushID(static_cast<int>(attr.GetPath().GetHash())); ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()));
ImGui::PushItemWidth(-FLT_MIN); ImGui::PushItemWidth(-FLT_MIN);
// Check for allowedTokens first → Combo (mirrors DrawTfToken in usdtweak) VtValue allowedTokens;
pxr::VtValue allowedTokens; attr.GetMetadata(TfToken("allowedTokens"), &allowedTokens);
attr.GetMetadata(pxr::TfToken("allowedTokens"), &allowedTokens);
pxr::VtValue val; VtValue val;
bool hasVal = attr.Get(&val, m_displayTime); bool hasVal = attr.Get(&val, m_displayTime);
if (!allowedTokens.IsEmpty() && if (!allowedTokens.IsEmpty() &&
allowedTokens.IsHolding<pxr::VtArray<pxr::TfToken>>()) allowedTokens.IsHolding<VtArray<TfToken>>())
{ {
const auto& tokens = allowedTokens.UncheckedGet<pxr::VtArray<pxr::TfToken>>(); const auto& tokens = allowedTokens.UncheckedGet<VtArray<TfToken>>();
std::string cur = (hasVal && val.IsHolding<pxr::TfToken>()) std::string cur = (hasVal && val.IsHolding<TfToken>())
? val.UncheckedGet<pxr::TfToken>().GetString() : ""; ? val.UncheckedGet<TfToken>().GetString() : "";
if (ImGui::BeginCombo("##tok", cur.c_str())) { if (ImGui::BeginCombo("##tok", cur.c_str())) {
for (const auto& tok : tokens) { for (const auto& tok : tokens) {
bool sel = (tok.GetString() == cur); bool sel = (tok.GetString() == cur);
@@ -1178,8 +1558,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::EndCombo(); ImGui::EndCombo();
} }
} else if (hasVal) { } else if (hasVal) {
// Type-aware widget dispatch (mirrors DrawVtValue in usdtweak) VtValue modified = DrawVtValueWidget(attr, val);
pxr::VtValue modified = DrawVtValueWidget(attr, val);
if (!modified.IsEmpty()) if (!modified.IsEmpty())
attr.Set(modified, m_editTime); attr.Set(modified, m_editTime);
} else if (attr.HasAuthoredConnections()) { } else if (attr.HasAuthoredConnections()) {
@@ -1198,11 +1577,11 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::PopID(); ImGui::PopID();
} }
// ---- Relationships (mirrors usdtweak relationship loop) ---- // ── Relationships ────────────────────────────────────────────────────────
for (const auto& rel : prim.GetRelationships()) { for (const auto& rel : rels) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(r)" // Col 0 (r) mini-button (no key button for relationships)
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
ImGui::PushID(uid++); ImGui::PushID(uid++);
DrawPropertyMiniButton("(r)", DrawPropertyMiniButton("(r)",
@@ -1210,15 +1589,41 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
rel.GetPath().GetString().c_str()); rel.GetPath().GetString().c_str());
ImGui::PopID(); ImGui::PopID();
// Col 1 display name, coloured by authored state (mirrors DrawUsdRelationshipDisplayName) // Col 1 display name + right-click context menu
ImGui::TableSetColumnIndex(1); ImGui::TableSetColumnIndex(1);
ImGui::PushID(static_cast<int>(rel.GetPath().GetHash()) ^ 0x100);
ImVec4 relColor = rel.IsAuthored() ImVec4 relColor = rel.IsAuthored()
? ImVec4(0.6f, 0.9f, 1.f, 1.f) ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.f); : ImVec4(0.5f, 0.5f, 0.5f, 1.f);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str()); ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str());
if (ImGui::BeginPopupContextItem("##relCtx")) {
if (ImGui::MenuItem("Copy path"))
ImGui::SetClipboardText(rel.GetPath().GetString().c_str());
ImGui::Separator();
ImGui::BeginDisabled(!rel.IsAuthored());
if (ImGui::MenuItem("Clear Override")) {
SdfPathVector oldTargets;
rel.GetTargets(&oldTargets);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Clear Override",
[rel]() mutable { rel.ClearTargets(false); },
[rel, tgts = oldTargets]() mutable {
for (const auto& p : tgts) rel.AddTarget(p);
}
));
} else {
rel.ClearTargets(false);
}
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
ImGui::PopID();
// Col 2 target list: single row when collapsed, expand for multiple // Col 2 target list
ImGui::TableSetColumnIndex(2); ImGui::TableSetColumnIndex(2);
SdfPathVector targets; SdfPathVector targets;
rel.GetTargets(&targets); rel.GetTargets(&targets);
@@ -1253,6 +1658,113 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::EndTable(); ImGui::EndTable();
} }
// ---------------------------------------------------------------------------
// Unified properties table (attributes + relationships)
//
// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties:
// • Flags: SizingFixedFit | RowBg — NO borders, NO header row
// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state
// • Col 1 (fixed 140px) : property display name (namespace:basename)
// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN)
// ---------------------------------------------------------------------------
void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
using namespace pxr;
// Build attr-name → category label map from schema registry
std::unordered_map<std::string, std::string> attrToCategory;
const UsdSchemaRegistry& reg = UsdSchemaRegistry::GetInstance();
if (!m_primType.empty()) {
if (const UsdPrimDefinition* typeDef =
reg.FindConcretePrimDefinition(TfToken(m_primType))) {
for (const TfToken& prop : typeDef->GetPropertyNames())
attrToCategory.emplace(prop.GetString(), m_primType);
}
}
for (const auto& schemaToken : prim.GetAppliedSchemas()) {
std::string s = schemaToken.GetString();
// Multi-apply tokens look like "CollectionAPI:name"; strip instance suffix
auto pos = s.find(':');
std::string baseName = (pos != std::string::npos) ? s.substr(0, pos) : s;
if (const UsdPrimDefinition* apiDef =
reg.FindAppliedAPIPrimDefinition(TfToken(baseName))) {
for (const TfToken& prop : apiDef->GetPropertyNames())
attrToCategory.emplace(prop.GetString(), baseName); // first writer wins
}
}
// Bucket attrs and rels into ordered category groups
struct PropGroup {
std::vector<UsdAttribute> attrs;
std::vector<UsdRelationship> rels;
};
std::vector<std::string> orderedCats;
std::set<std::string> seenCats;
std::unordered_map<std::string, PropGroup> groups;
auto addCat = [&](const std::string& cat) {
if (seenCats.insert(cat).second) orderedCats.push_back(cat);
};
if (!m_primType.empty()) addCat(m_primType);
const std::string fallback = m_primType.empty() ? "Properties" : m_primType;
for (auto attr : prim.GetAttributes()) {
const std::string name = attr.GetName().GetString();
if (TfStringStartsWith(name, "xformOp:")) continue; // shown in TRS section
auto it = attrToCategory.find(name);
std::string cat;
if (it != attrToCategory.end()) {
cat = it->second;
} else {
std::string ns = attr.GetNamespace().GetString();
if (ns.empty()) {
cat = fallback;
} else {
auto c = ns.find(':');
cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
}
}
addCat(cat);
groups[cat].attrs.push_back(attr);
}
for (const auto& rel : prim.GetRelationships()) {
std::string ns = rel.GetNamespace().GetString();
std::string cat;
if (ns.empty()) {
cat = fallback;
} else {
auto c = ns.find(':');
cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
}
addCat(cat);
groups[cat].rels.push_back(rel);
}
// Sort: primType first (already inserted), remaining alphabetically
if (orderedCats.size() > 1) {
size_t firstSort = m_primType.empty() ? 0 : 1;
std::sort(orderedCats.begin() + firstSort, orderedCats.end());
}
// Render each category under a collapsing header
const UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
for (const auto& cat : orderedCats) {
auto& group = groups[cat];
if (group.attrs.empty() && group.rels.empty()) continue;
ImGui::PushID(cat.c_str());
if (ImGui::CollapsingHeader(cat.c_str(), ImGuiTreeNodeFlags_DefaultOpen))
RenderAttrRelSubTable(group.attrs, group.rels, editTarget);
ImGui::PopID();
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Main render entry point // Main render entry point
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1281,14 +1793,29 @@ void PropertyPanel::Render() {
ImGui::Separator(); ImGui::Separator();
// Scrollable body variant sets → transform → all properties // Scrollable body composition arcs → variant sets → transform → all properties
ImGui::BeginChild("##body"); ImGui::BeginChild("##body");
if (prim.HasAuthoredReferences() || prim.HasAuthoredPayloads()) {
RenderCompositionArcsSection(prim);
ImGui::Separator();
}
if (!prim.GetAssetInfo().empty()) {
RenderAssetInfoSection(prim);
ImGui::Separator();
}
if (prim.HasVariantSets()) { if (prim.HasVariantSets()) {
RenderVariantSetsSection(prim); RenderVariantSetsSection(prim);
ImGui::Separator(); ImGui::Separator();
} }
if (prim.HasAPI<pxr::UsdShadeMaterialBindingAPI>()) {
RenderMaterialBindSection(prim);
ImGui::Separator();
}
if (m_hasXform) { if (m_hasXform) {
RenderTransformSection(); RenderTransformSection();
ImGui::Separator(); ImGui::Separator();
+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
+394 -9
View File
@@ -8,6 +8,7 @@
#include "../core/commands/RenamePrimCommand.h" #include "../core/commands/RenamePrimCommand.h"
#include "../core/commands/ReparentPrimCommand.h" #include "../core/commands/ReparentPrimCommand.h"
#include "../core/commands/GroupPrimsCommand.h" #include "../core/commands/GroupPrimsCommand.h"
#include "../core/commands/LayerCommands.h"
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h> #include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/references.h> #include <pxr/usd/usd/references.h>
@@ -86,9 +87,58 @@ void SceneHierarchyPanel::SetSelectedPathFromClick(const std::string& path) {
m_primarySelectedPath = path; m_primarySelectedPath = path;
m_primarySdfPath = path.empty() ? SdfPath() : SdfPath(path); m_primarySdfPath = path.empty() ? SdfPath() : SdfPath(path);
if (!path.empty()) m_selectedPaths.insert(path); if (!path.empty()) m_selectedPaths.insert(path);
// No scroll — user clicked the item directly, it's already visible. if (!path.empty()) m_rangeAnchorPath = SdfPath(path);
m_scrollToSelected = false; m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(path); if (m_onPrimSelected) m_onPrimSelected(path);
if (m_onPrimsSelected) {
std::vector<std::string> v;
if (!path.empty()) v.push_back(path);
m_onPrimsSelected(v);
}
}
void SceneHierarchyPanel::ToggleSelectionFromClick(const std::string& path) {
if (m_selectedPaths.count(path)) {
m_selectedPaths.erase(path);
if (m_primarySelectedPath == path) {
m_primarySelectedPath = m_selectedPaths.empty() ? "" : *m_selectedPaths.begin();
m_primarySdfPath = m_primarySelectedPath.empty()
? SdfPath() : SdfPath(m_primarySelectedPath);
}
} else {
m_selectedPaths.insert(path);
m_primarySelectedPath = path;
m_primarySdfPath = SdfPath(path);
m_rangeAnchorPath = SdfPath(path);
}
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(m_primarySelectedPath);
if (m_onPrimsSelected) {
std::vector<std::string> v(m_selectedPaths.begin(), m_selectedPaths.end());
m_onPrimsSelected(v);
}
}
void SceneHierarchyPanel::RangeSelectToPath(const SdfPath& path) {
auto it1 = std::find(m_visiblePrimOrder.begin(), m_visiblePrimOrder.end(), m_rangeAnchorPath);
auto it2 = std::find(m_visiblePrimOrder.begin(), m_visiblePrimOrder.end(), path);
if (it1 != m_visiblePrimOrder.end() && it2 != m_visiblePrimOrder.end()) {
int idx1 = static_cast<int>(it1 - m_visiblePrimOrder.begin());
int idx2 = static_cast<int>(it2 - m_visiblePrimOrder.begin());
if (idx1 > idx2) std::swap(idx1, idx2);
for (int i = idx1; i <= idx2; ++i)
m_selectedPaths.insert(m_visiblePrimOrder[i].GetString());
} else {
m_selectedPaths.insert(path.GetString());
}
m_primarySelectedPath = path.GetString();
m_primarySdfPath = path;
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(m_primarySelectedPath);
if (m_onPrimsSelected) {
std::vector<std::string> v(m_selectedPaths.begin(), m_selectedPaths.end());
m_onPrimsSelected(v);
}
} }
const char* SceneHierarchyPanel::GetPrimTypeIcon(const UsdPrim& prim) const { const char* SceneHierarchyPanel::GetPrimTypeIcon(const UsdPrim& prim) const {
@@ -185,6 +235,26 @@ void SceneHierarchyPanel::HandleKeyboardShortcuts() {
SetSelectedPathFromClick(groupPath.GetString()); SetSelectedPathFromClick(groupPath.GetString());
} }
// Ctrl+H — hide selected prims (visibility = invisible).
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_H, false) &&
!m_selectedPaths.empty() && m_stage) {
for (const auto& pathStr : m_selectedPaths) {
UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(pathStr));
if (prim.IsValid() && prim.IsA<UsdGeomImageable>())
UsdGeomImageable(prim).GetVisibilityAttr().Set(UsdGeomTokens->invisible);
}
}
// Shift+H — show selected prims (visibility = inherited).
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_H, false) &&
!m_selectedPaths.empty() && m_stage) {
for (const auto& pathStr : m_selectedPaths) {
UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(pathStr));
if (prim.IsValid() && prim.IsA<UsdGeomImageable>())
UsdGeomImageable(prim).GetVisibilityAttr().Set(UsdGeomTokens->inherited);
}
}
// Delete — open the remove-prim confirmation modal. // Delete — open the remove-prim confirmation modal.
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false) && !m_primarySdfPath.IsEmpty()) { if (ImGui::IsKeyPressed(ImGuiKey_Delete, false) && !m_primarySdfPath.IsEmpty()) {
auto rootLayer = m_stage->GetRootLayer(); auto rootLayer = m_stage->GetRootLayer();
@@ -240,6 +310,8 @@ void SceneHierarchyPanel::Render() {
} }
} }
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
ImGui::BeginDisabled(renderLayerActive);
ImGui::SetNextItemWidth(-1.0f); ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(), if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
ImGuiComboFlags_HeightRegular)) { ImGuiComboFlags_HeightRegular)) {
@@ -257,15 +329,25 @@ void SceneHierarchyPanel::Render() {
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
bool comboHovered = ImGui::IsItemHovered();
ImGui::EndDisabled();
if (renderLayerActive && comboHovered)
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Spacing(); ImGui::Spacing();
} }
RenderSublayerSection();
ImGui::Spacing();
auto paths = m_propertyManager->GetPrimPaths(); auto paths = m_propertyManager->GetPrimPaths();
if (paths.empty()) { if (paths.empty()) {
ImGui::TextDisabled("No prims in stage"); ImGui::TextDisabled("No prims in stage");
} else { } else {
UsdPrim root = m_stage->GetPseudoRoot(); UsdPrim root = m_stage->GetPseudoRoot();
// Rebuilt each frame as RenderPrimNode visits prims — used for range select.
m_visiblePrimOrder.clear();
// Rebuild local-layer set once per frame (used by RenderPrimNode to // Rebuild local-layer set once per frame (used by RenderPrimNode to
// detect attribute overrides). GetLayerStack() returns only the stage's // detect attribute overrides). GetLayerStack() returns only the stage's
// own layers — root layer, sublayers, session layer — NOT reference layers. // own layers — root layer, sublayers, session layer — NOT reference layers.
@@ -288,12 +370,14 @@ void SceneHierarchyPanel::Render() {
ImGuiTableFlags_RowBg | ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingFixedFit; ImGuiTableFlags_SizingFixedFit;
if (ImGui::BeginTable("##primtree", 4, tblFlags)) { const float kSpecW = ImGui::CalcTextSize("over").x + 2.f;
// Col 0 stretches; cols 1-3 are small fixed-width icon columns. if (ImGui::BeginTable("##primtree", 5, tblFlags)) {
// Col 0 stretches; cols 1-4 are small fixed-width columns.
ImGui::TableSetupColumn("##prim", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("##prim", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##type", ImGuiTableColumnFlags_WidthFixed, kIconW); ImGui::TableSetupColumn("##type", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##vis", ImGuiTableColumnFlags_WidthFixed, kIconW); ImGui::TableSetupColumn("##vis", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##ref", ImGuiTableColumnFlags_WidthFixed, kIconW); ImGui::TableSetupColumn("##ref", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##spec", ImGuiTableColumnFlags_WidthFixed, kSpecW);
for (const auto& child : root.GetChildren()) for (const auto& child : root.GetChildren())
RenderPrimNode(child); RenderPrimNode(child);
@@ -508,6 +592,272 @@ void SceneHierarchyPanel::Render() {
ProcessPendingReplaceRef(); ProcessPendingReplaceRef();
} }
// ---------------------------------------------------------------------------
void SceneHierarchyPanel::RenderSublayerSection() {
if (!ImGui::CollapsingHeader("Sublayers", ImGuiTreeNodeFlags_DefaultOpen))
return;
if (!m_layerManager) {
ImGui::TextDisabled(" No layer manager");
return;
}
auto sublayers = m_layerManager->GetSublayers();
if (sublayers.empty()) {
ImGui::TextDisabled(" No sublayers");
return;
}
const float rowHeight = ImGui::GetTextLineHeightWithSpacing();
const int visibleRows = std::min<int>(static_cast<int>(sublayers.size()), 6);
float tableHeight = rowHeight * (visibleRows + 1); // +1 for header row
if (!ImGui::BeginTable("HierarchySublayerTable", 6,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
ImVec2(0, tableHeight)))
return;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("##drag", ImGuiTableColumnFlags_WidthFixed, 14.0f);
ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f);
ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 55.0f);
ImGui::TableSetupColumn("##order", ImGuiTableColumnFlags_WidthFixed, 44.0f);
// Custom header row: pen icon for the ET column, text for the rest.
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
ImGui::TableSetColumnIndex(2); ImGui::TableHeader("Name");
ImGui::TableSetColumnIndex(3);
{
float cellW = ImGui::GetContentRegionAvail().x;
float iconW = 14.0f;
float off = (cellW - iconW) * 0.5f;
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
if (m_iconManager)
ImGui::Image(ImTextureRef(m_iconManager->Get(Icon::Pen)), ImVec2(iconW, iconW));
else
ImGui::TextUnformatted("ET");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Edit Target");
}
ImGui::TableSetColumnIndex(4); ImGui::TableHeader("Muted");
ImGui::TableSetColumnIndex(5); ImGui::TableHeader("Order");
for (int i = 0; i < static_cast<int>(sublayers.size()); i++) {
const auto& li = sublayers[i];
ImGui::TableNextRow();
ImGui::PushID(i);
// Col 0: invisible span-all Selectable for row selection / drag / context menu.
ImGui::TableSetColumnIndex(0);
ImVec2 rowStart = ImGui::GetCursorScreenPos(); // save before Selectable moves cursor
bool isSelected = (m_sublayerSelectedIdx == i);
if (ImGui::Selectable("##row", isSelected,
ImGuiSelectableFlags_SpanAllColumns |
ImGuiSelectableFlags_AllowOverlap,
ImVec2(0, 0)))
m_sublayerSelectedIdx = i;
// Context menu: must come immediately after the span-all Selectable so
// BeginPopupContextItem sees it as the "last item" and responds to
// right-click anywhere in the row.
RenderSublayerContextMenu(i, sublayers);
// Drag source: also attached to the Selectable (left-button drag).
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
ImGui::SetDragDropPayload("HIER_SUBLAYER_IDX", &i, sizeof(int));
ImGui::Text("%s", li.displayName.c_str());
ImGui::EndDragDropSource();
}
// Drop target on the Selectable.
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("HIER_SUBLAYER_IDX")) {
int src = *static_cast<const int*>(payload->Data);
int dst = i;
if (src != dst) {
std::vector<std::string> before, after;
for (const auto& sl : sublayers) before.push_back(sl.identifier);
after = before;
std::string moved = after[src];
after.erase(after.begin() + src);
after.insert(after.begin() + dst, moved);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
m_layerManager, before, after));
} else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
}
ImGui::EndDragDropTarget();
}
// "=" drag handle: overlay at col 0 using the saved screen position so it
// sits inside the row, not below it (TableSetColumnIndex only resets X).
ImGui::SetCursorScreenPos(rowStart);
ImGui::TextDisabled("=");
// Col 1: dirty indicator
ImGui::TableSetColumnIndex(1);
if (li.isDirty)
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
else
ImGui::TextDisabled(" ");
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
// Col 2: layer name
ImGui::TableSetColumnIndex(2);
ImVec4 nameColor = li.isMuted
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
if (ImGui::IsItemHovered() && !li.realPath.empty())
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;
// unchecking does nothing (there is always an active edit target).
ImGui::TableSetColumnIndex(3);
{
bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar();
if (ImGui::IsItemHovered())
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 — disabled for render-layer sublayers, which
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
ImGui::TableSetColumnIndex(4);
{
bool muted = li.isMuted;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(isRenderLayer);
if (ImGui::Checkbox("##muted", &muted)) {
if (muted) m_layerManager->MuteLayer(li.identifier);
else m_layerManager->UnmuteLayer(li.identifier);
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
}
// Col 5: up/down reorder icon buttons.
ImGui::TableSetColumnIndex(5);
{
bool canUp = (i > 0);
bool canDown = (i < static_cast<int>(sublayers.size()) - 1);
auto buildBeforeAfter = [&](int a, int b,
std::vector<std::string>& before,
std::vector<std::string>& after) {
for (const auto& sl : sublayers) before.push_back(sl.identifier);
after = before;
std::swap(after[a], after[b]);
};
ImVec2 iconSize(14.0f, 14.0f);
ImGui::BeginDisabled(!canUp);
ImTextureID upTex = m_iconManager ? m_iconManager->Get(Icon::ChevronUp) : ImTextureID_Invalid;
if (m_iconManager ? ImGui::ImageButton("##up", ImTextureRef(upTex), iconSize)
: ImGui::ArrowButton("##up", ImGuiDir_Up)) {
std::vector<std::string> before, after;
buildBeforeAfter(i, i - 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Up");
ImGui::SameLine(0.0f, 2.0f);
ImGui::BeginDisabled(!canDown);
ImTextureID downTex = m_iconManager ? m_iconManager->Get(Icon::ChevronDown) : ImTextureID_Invalid;
if (m_iconManager ? ImGui::ImageButton("##down", ImTextureRef(downTex), iconSize)
: ImGui::ArrowButton("##down", ImGuiDir_Down)) {
std::vector<std::string> before, after;
buildBeforeAfter(i, i + 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Down");
}
ImGui::PopID();
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
void SceneHierarchyPanel::RenderSublayerContextMenu(int i,
const std::vector<LayerInfo>& sublayers) {
if (!ImGui::BeginPopupContextItem(("ctx_sublayer_" + std::to_string(i)).c_str()))
return;
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", nullptr, false, !isRenderLayer)) {
if (li.isMuted) m_layerManager->UnmuteLayer(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", nullptr, false, !renderLayerActive))
m_layerManager->SetEditTarget(li.identifier);
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Separator();
if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
else
m_layerManager->RemoveSublayer(i);
}
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
ImGui::EndPopup();
}
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) { void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
if (!prim.IsValid()) return; if (!prim.IsValid()) return;
@@ -524,6 +874,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
bool isInvisible = false; bool isInvisible = false;
bool hasRefs = prim.HasAuthoredReferences(); bool hasRefs = prim.HasAuthoredReferences();
bool hasChildren = !prim.GetChildren().empty(); bool hasChildren = !prim.GetChildren().empty();
SdfSpecifier spec = prim.GetSpecifier();
if (isImageable) { if (isImageable) {
UsdGeomImageable img(prim); UsdGeomImageable img(prim);
@@ -565,6 +916,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
// ──────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────
ImGui::TableNextRow(); ImGui::TableNextRow();
m_visiblePrimOrder.push_back(primPath); // for Shift+LMB range select
ImGui::TableNextColumn(); // Col 0 — prim name + tree arrow ImGui::TableNextColumn(); // Col 0 — prim name + tree arrow
ImGui::PushID(primStr.c_str()); ImGui::PushID(primStr.c_str());
@@ -642,6 +994,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::TableNextColumn(); // Col 1 ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2 ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3 ImGui::TableNextColumn(); // Col 3
ImGui::TableNextColumn(); // Col 4
if (open && hasChildren) if (open && hasChildren)
ImGui::TreePop(); ImGui::TreePop();
ImGui::PopID(); ImGui::PopID();
@@ -655,9 +1008,16 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
} }
if (!isRenaming) { if (!isRenaming) {
// Selection on click (not on toggle arrow). // Selection on LMB click (not on toggle arrow).
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) // Shift+LMB: range-select from anchor. Ctrl+LMB: toggle. Plain: replace.
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
if (ImGui::GetIO().KeyShift)
RangeSelectToPath(primPath);
else if (ImGui::GetIO().KeyCtrl)
ToggleSelectionFromClick(primStr);
else
SetSelectedPathFromClick(primStr); SetSelectedPathFromClick(primStr);
}
// Double-click to start inline rename. // Double-click to start inline rename.
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
@@ -705,7 +1065,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::EndDragDropTarget(); ImGui::EndDragDropTarget();
} }
// Context menu (must follow the last widget = the tree node). // Context menu on RMB.
RenderContextMenu(prim); RenderContextMenu(prim);
// A context menu op (Unparent, Group, etc.) may have moved this prim, // A context menu op (Unparent, Group, etc.) may have moved this prim,
@@ -714,6 +1074,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::TableNextColumn(); // Col 1 ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2 ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3 ImGui::TableNextColumn(); // Col 3
ImGui::TableNextColumn(); // Col 4
if (open && hasChildren) if (open && hasChildren)
ImGui::TreePop(); ImGui::TreePop();
ImGui::PopID(); ImGui::PopID();
@@ -740,8 +1101,9 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
const float iconSz = ImGui::GetTextLineHeight(); const float iconSz = ImGui::GetTextLineHeight();
const ImVec2 iconVec(iconSz, iconSz); const ImVec2 iconVec(iconSz, iconSz);
Icon visIcon = isInvisible ? Icon::EyeSlash : Icon::Eye; Icon visIcon = isInvisible ? Icon::EyeSlash : Icon::Eye;
ImVec4 visTint = isInvisible ? ImVec4(0.45f, 0.45f, 0.45f, 0.6f) // Invisible: EyeSlash in orange; Visible: Eye at full alpha (dimmer tint).
: ImVec4(0.9f, 0.9f, 0.9f, 1.0f); ImVec4 visTint = isInvisible ? ImVec4(1.00f, 0.60f, 0.15f, 1.0f)
: ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
ImTextureID id = m_iconManager ? m_iconManager->Get(visIcon) : ImTextureID_Invalid; ImTextureID id = m_iconManager ? m_iconManager->Get(visIcon) : ImTextureID_Invalid;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
@@ -750,7 +1112,8 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f);
if (ImGui::ImageButton("##vis", ImTextureRef(id), iconVec, std::string visId = "##vis_" + primStr;
if (ImGui::ImageButton(visId.c_str(), ImTextureRef(id), iconVec,
ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), visTint)) { ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), visTint)) {
try { try {
UsdGeomImageable img(prim); UsdGeomImageable img(prim);
@@ -789,6 +1152,28 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::SetTooltip("Has references"); ImGui::SetTooltip("Has references");
} }
// ── Col 4: Specifier (def / over / class) ──────────────────────────────
ImGui::TableNextColumn();
{
const char* label = nullptr;
ImVec4 color;
switch (spec) {
case SdfSpecifierOver:
label = "over";
color = ImVec4(1.00f, 0.60f, 0.10f, isActive ? 1.0f : 0.45f);
break;
case SdfSpecifierClass:
label = "class";
color = ImVec4(0.40f, 0.70f, 1.00f, isActive ? 1.0f : 0.45f);
break;
default: // SdfSpecifierDef
label = "def";
color = ImVec4(0.45f, 0.45f, 0.45f, isActive ? 0.55f : 0.30f);
break;
}
ImGui::TextColored(color, "%s", label);
}
// ── Recurse into children ─────────────────────────────────────────────── // ── Recurse into children ───────────────────────────────────────────────
// TreePop must be called in the SAME column as TreeNodeEx (col 0). // TreePop must be called in the SAME column as TreeNodeEx (col 0).
// Since we called TableNextColumn three more times above, we must move // Since we called TableNextColumn three more times above, we must move
+29 -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,6 +74,9 @@ 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; }
@@ -76,10 +91,13 @@ private:
void RenderRemovePrimModal(); void RenderRemovePrimModal();
void RenderArcModal(); 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;
@@ -91,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.
@@ -121,6 +147,8 @@ private:
bool m_renameJustStarted = false; bool m_renameJustStarted = false;
void HandleKeyboardShortcuts(); void HandleKeyboardShortcuts();
void ToggleSelectionFromClick(const std::string& path);
void RangeSelectToPath(const pxr::SdfPath& path);
SdfPath FindUniqueChildPath(const SdfPath& parent, const std::string& baseName); SdfPath FindUniqueChildPath(const SdfPath& parent, const std::string& baseName);
}; };
+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;
}; };
+66 -13
View File
@@ -67,6 +67,7 @@ void ViewportPanel::WireCallbacks(int i)
BroadcastSelection(); BroadcastSelection();
if (OnPrimsPickedRect) OnPrimsPickedRect(paths); if (OnPrimsPickedRect) OnPrimsPickedRect(paths);
}; };
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -137,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
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -235,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();
@@ -271,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) {
@@ -278,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;
@@ -303,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;
@@ -383,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
@@ -610,6 +633,34 @@ 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 // Settings persistence
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -628,6 +679,11 @@ void ViewportPanel::SaveSettings(const std::string& path) const
f << "\n[Tile" << i << "]\n"; f << "\n[Tile" << i << "]\n";
f << "RenderDelegate=" << s.renderDelegate << "\n"; f << "RenderDelegate=" << s.renderDelegate << "\n";
f << "ShowGrid=" << (s.showGrid ? 1 : 0) << "\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 << "AAEnabled=" << (s.aaEnabled ? 1 : 0) << "\n";
f << "BgColorR=" << s.bgColorR << "\n"; f << "BgColorR=" << s.bgColorR << "\n";
f << "BgColorG=" << s.bgColorG << "\n"; f << "BgColorG=" << s.bgColorG << "\n";
@@ -636,11 +692,7 @@ void ViewportPanel::SaveSettings(const std::string& path) const
f << "AmbientLightOnly=" << (s.ambientLightOnly ? 1 : 0) << "\n"; f << "AmbientLightOnly=" << (s.ambientLightOnly ? 1 : 0) << "\n";
f << "DomeLightEnabled=" << (s.domeLightEnabled ? 1 : 0) << "\n"; f << "DomeLightEnabled=" << (s.domeLightEnabled ? 1 : 0) << "\n";
f << "ShadingMode=" << s.shadingMode << "\n"; f << "ShadingMode=" << s.shadingMode << "\n";
f << "ColorCorrectionMode=" << s.colorCorrectionMode << "\n"; f << "CullStyle=" << s.cullStyle << "\n";
f << "OcioDisplay=" << s.ocioDisplay << "\n";
f << "OcioView=" << s.ocioView << "\n";
f << "OcioColorSpace=" << s.ocioColorSpace << "\n";
f << "OcioLook=" << s.ocioLook << "\n";
} }
} }
@@ -699,6 +751,11 @@ void ViewportPanel::LoadSettings(const std::string& path)
auto& ts = tileSettings[section]; auto& ts = tileSettings[section];
if (key == "RenderDelegate") ts.renderDelegate = val; if (key == "RenderDelegate") ts.renderDelegate = val;
else if (key == "ShowGrid") ts.showGrid = (val == "1"); 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 == "AAEnabled") ts.aaEnabled = (val == "1");
else if (key == "BgColorR") ts.bgColorR = toFloat(val, ts.bgColorR); else if (key == "BgColorR") ts.bgColorR = toFloat(val, ts.bgColorR);
else if (key == "BgColorG") ts.bgColorG = toFloat(val, ts.bgColorG); else if (key == "BgColorG") ts.bgColorG = toFloat(val, ts.bgColorG);
@@ -707,11 +764,7 @@ void ViewportPanel::LoadSettings(const std::string& path)
else if (key == "AmbientLightOnly") ts.ambientLightOnly = (val == "1"); else if (key == "AmbientLightOnly") ts.ambientLightOnly = (val == "1");
else if (key == "DomeLightEnabled") ts.domeLightEnabled = (val == "1"); else if (key == "DomeLightEnabled") ts.domeLightEnabled = (val == "1");
else if (key == "ShadingMode") ts.shadingMode = toInt(val, ts.shadingMode); else if (key == "ShadingMode") ts.shadingMode = toInt(val, ts.shadingMode);
else if (key == "ColorCorrectionMode") ts.colorCorrectionMode = toInt(val, ts.colorCorrectionMode); else if (key == "CullStyle") ts.cullStyle = toInt(val, ts.cullStyle);
else if (key == "OcioDisplay") ts.ocioDisplay = val;
else if (key == "OcioView") ts.ocioView = val;
else if (key == "OcioColorSpace") ts.ocioColorSpace = val;
else if (key == "OcioLook") ts.ocioLook = val;
} }
} }
+14 -3
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,9 +67,18 @@ 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 ───────────────────────────────────────────────── // ── Settings persistence ─────────────────────────────────────────────────
/// Load layout and per-tile render settings from a simple INI file. /// Load layout and per-tile render settings from a simple INI file.
/// Safe to call immediately after construction (before any Render call).
void LoadSettings(const std::string& path); void LoadSettings(const std::string& path);
/// Save current layout and per-tile render settings to a simple INI file. /// Save current layout and per-tile render settings to a simple INI file.
void SaveSettings(const std::string& path) const; void SaveSettings(const std::string& path) const;
@@ -90,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.
+215 -138
View File
@@ -1,6 +1,5 @@
#include "ViewportTile.h" #include "ViewportTile.h"
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/OcioConfigParser.h"
#include <pxr/usd/usdGeom/camera.h> #include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h> #include <pxr/usd/usdGeom/xformCommonAPI.h>
@@ -17,6 +16,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <map>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -94,6 +94,11 @@ ViewportTileSettings ViewportTile::GetSettings() const
ViewportTileSettings s; ViewportTileSettings s;
s.renderDelegate = m_renderer.GetCurrentRendererId().GetString(); s.renderDelegate = m_renderer.GetCurrentRendererId().GetString();
s.showGrid = m_renderer.ShowGrid(); 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(); s.aaEnabled = m_renderer.GetAAEnabled();
const pxr::GfVec3f& bg = m_renderer.GetBackgroundColor(); const pxr::GfVec3f& bg = m_renderer.GetBackgroundColor();
s.bgColorR = bg[0]; s.bgColorG = bg[1]; s.bgColorB = bg[2]; s.bgColorR = bg[0]; s.bgColorG = bg[1]; s.bgColorB = bg[2];
@@ -101,11 +106,7 @@ ViewportTileSettings ViewportTile::GetSettings() const
s.ambientLightOnly = m_renderer.GetAmbientLightOnly(); s.ambientLightOnly = m_renderer.GetAmbientLightOnly();
s.domeLightEnabled = m_renderer.GetDomeLightEnabled(); s.domeLightEnabled = m_renderer.GetDomeLightEnabled();
s.shadingMode = static_cast<int>(m_renderer.GetShadingMode()); s.shadingMode = static_cast<int>(m_renderer.GetShadingMode());
s.colorCorrectionMode = static_cast<int>(m_renderer.GetColorCorrectionMode()); s.cullStyle = static_cast<int>(m_renderer.GetCullStyle());
s.ocioDisplay = m_renderer.GetOcioDisplay();
s.ocioView = m_renderer.GetOcioView();
s.ocioColorSpace = m_renderer.GetOcioColorSpace();
s.ocioLook = m_renderer.GetOcioLook();
return s; return s;
} }
@@ -114,18 +115,19 @@ void ViewportTile::ApplySettings(const ViewportTileSettings& s)
if (!s.renderDelegate.empty()) if (!s.renderDelegate.empty())
m_renderer.SetRendererPlugin(pxr::TfToken(s.renderDelegate)); m_renderer.SetRendererPlugin(pxr::TfToken(s.renderDelegate));
m_renderer.SetShowGrid(s.showGrid); 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.SetAAEnabled(s.aaEnabled);
m_renderer.SetBackgroundColor(pxr::GfVec3f(s.bgColorR, s.bgColorG, s.bgColorB)); m_renderer.SetBackgroundColor(pxr::GfVec3f(s.bgColorR, s.bgColorG, s.bgColorB));
m_renderer.SetBBoxMode(static_cast<BBoxMode>(s.bboxMode)); m_renderer.SetBBoxMode(static_cast<BBoxMode>(s.bboxMode));
m_renderer.SetAmbientLightOnly(s.ambientLightOnly); m_renderer.SetAmbientLightOnly(s.ambientLightOnly);
m_renderer.SetDomeLightEnabled(s.domeLightEnabled); m_renderer.SetDomeLightEnabled(s.domeLightEnabled);
m_renderer.SetShadingMode(static_cast<ShadingMode>(s.shadingMode)); m_renderer.SetShadingMode(static_cast<ShadingMode>(s.shadingMode));
m_renderer.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(s.colorCorrectionMode)); m_renderer.SetCullStyle(static_cast<CullStyle>(s.cullStyle));
m_renderer.SetOcioDisplay(s.ocioDisplay);
m_renderer.SetOcioView(s.ocioView);
m_renderer.SetOcioColorSpace(s.ocioColorSpace);
m_renderer.SetOcioLook(s.ocioLook);
m_ocioFieldsSynced = false; // re-seed OCIO edit buffers from restored values
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -750,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();
@@ -912,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("|");
@@ -997,124 +1005,6 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
if (ImGui::BeginPopupContextItem(gearPopupId.c_str(), if (ImGui::BeginPopupContextItem(gearPopupId.c_str(),
ImGuiPopupFlags_MouseButtonLeft)) ImGuiPopupFlags_MouseButtonLeft))
{ {
// -- Color correction --------------------------------------------
if (ImGui::BeginMenu("Color Correction")) {
ColorCorrectionMode ccm = m_renderer.GetColorCorrectionMode();
auto ccItem = [&](const char* label, ColorCorrectionMode mode) {
if (ImGui::MenuItem(label, nullptr, ccm == mode)) {
m_renderer.SetColorCorrectionMode(mode);
m_ocioFieldsSynced = false;
}
};
ccItem("Disabled", ColorCorrectionMode::Disabled);
ccItem("sRGB", ColorCorrectionMode::sRGB);
ccItem("OpenColorIO", ColorCorrectionMode::OpenColorIO);
if (ccm == ColorCorrectionMode::OpenColorIO) {
ImGui::Separator();
const OcioConfig& ocfg = GetCurrentOcioConfig();
// Seed buffers from renderer, falling back to OCIO config defaults
if (!m_ocioFieldsSynced) {
std::string disp = m_renderer.GetOcioDisplay();
std::string view = m_renderer.GetOcioView();
if (disp.empty()) disp = ocfg.defaultDisplay;
if (view.empty()) view = ocfg.defaultView;
strncpy(m_ocioDisplayBuf, disp.c_str(), 127);
strncpy(m_ocioViewBuf, view.c_str(), 127);
strncpy(m_ocioColorSpaceBuf, m_renderer.GetOcioColorSpace().c_str(), 127);
strncpy(m_ocioLookBuf, m_renderer.GetOcioLook().c_str(), 127);
if (m_renderer.GetOcioDisplay().empty() && !disp.empty())
m_renderer.SetOcioDisplay(disp);
if (m_renderer.GetOcioView().empty() && !view.empty())
m_renderer.SetOcioView(view);
m_ocioFieldsSynced = true;
}
ImGui::PushItemWidth(200.f);
// Display combo
if (ImGui::BeginCombo("Display##ocio", m_ocioDisplayBuf)) {
for (const auto& d : ocfg.displays) {
bool sel = (d == m_ocioDisplayBuf);
if (ImGui::Selectable(d.c_str(), sel)) {
strncpy(m_ocioDisplayBuf, d.c_str(), 127);
m_renderer.SetOcioDisplay(d);
// Auto-select default view for new display
auto vit = ocfg.views.find(d);
if (vit != ocfg.views.end() && !vit->second.empty()) {
strncpy(m_ocioViewBuf, vit->second[0].c_str(), 127);
m_renderer.SetOcioView(vit->second[0]);
}
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// View combo — filtered by current display
{
static const std::vector<std::string> kEmpty;
auto vit = ocfg.views.find(std::string(m_ocioDisplayBuf));
const auto& views = (vit != ocfg.views.end()) ? vit->second : kEmpty;
if (ImGui::BeginCombo("View##ocio", m_ocioViewBuf)) {
for (const auto& v : views) {
bool sel = (v == m_ocioViewBuf);
if (ImGui::Selectable(v.c_str(), sel)) {
strncpy(m_ocioViewBuf, v.c_str(), 127);
m_renderer.SetOcioView(v);
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
// Color Space combo
if (ImGui::BeginCombo("Color Space##ocio",
m_ocioColorSpaceBuf[0] ? m_ocioColorSpaceBuf : "(default)")) {
if (ImGui::Selectable("(default)", m_ocioColorSpaceBuf[0] == '\0')) {
m_ocioColorSpaceBuf[0] = '\0';
m_renderer.SetOcioColorSpace("");
}
if (m_ocioColorSpaceBuf[0] == '\0') ImGui::SetItemDefaultFocus();
for (const auto& cs : ocfg.colorSpaces) {
bool sel = (cs == m_ocioColorSpaceBuf);
if (ImGui::Selectable(cs.c_str(), sel)) {
strncpy(m_ocioColorSpaceBuf, cs.c_str(), 127);
m_renderer.SetOcioColorSpace(cs);
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// Look combo
if (ImGui::BeginCombo("Look##ocio",
m_ocioLookBuf[0] ? m_ocioLookBuf : "(none)")) {
if (ImGui::Selectable("(none)", m_ocioLookBuf[0] == '\0')) {
m_ocioLookBuf[0] = '\0';
m_renderer.SetOcioLook("");
}
if (m_ocioLookBuf[0] == '\0') ImGui::SetItemDefaultFocus();
for (const auto& look : ocfg.looks) {
bool sel = (look == m_ocioLookBuf);
if (ImGui::Selectable(look.c_str(), sel)) {
strncpy(m_ocioLookBuf, look.c_str(), 127);
m_renderer.SetOcioLook(look);
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
}
ImGui::EndMenu();
}
ImGui::Separator();
// -- Shading mode ------------------------------------------------ // -- Shading mode ------------------------------------------------
if (ImGui::BeginMenu("Shading")) { if (ImGui::BeginMenu("Shading")) {
ShadingMode cur = m_renderer.GetShadingMode(); ShadingMode cur = m_renderer.GetShadingMode();
@@ -1130,6 +1020,35 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
ImGui::EndMenu(); 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 ---------------------------------------------------- // -- Lighting ----------------------------------------------------
if (ImGui::BeginMenu("Lighting")) { if (ImGui::BeginMenu("Lighting")) {
bool ambient = m_renderer.GetAmbientLightOnly(); bool ambient = m_renderer.GetAmbientLightOnly();
@@ -1152,6 +1071,23 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
if (ImGui::MenuItem("Show Grid", nullptr, grid)) if (ImGui::MenuItem("Show Grid", nullptr, grid))
m_renderer.SetShowGrid(!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(); bool aaItem = m_renderer.GetAAEnabled();
if (ImGui::MenuItem("Anti-Aliasing", nullptr, aaItem)) if (ImGui::MenuItem("Anti-Aliasing", nullptr, aaItem))
m_renderer.SetAAEnabled(!aaItem); m_renderer.SetAAEnabled(!aaItem);
@@ -1185,6 +1121,10 @@ void ViewportTile::RenderCompactToolbar(int tileIndex)
ImGui::EndMenu(); ImGui::EndMenu();
} }
ImGui::Separator();
if (ImGui::MenuItem("Renderer Settings..."))
m_showRendererSettings = true;
ImGui::EndPopup(); ImGui::EndPopup();
} }
} }
@@ -1423,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
+22 -17
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,23 +17,29 @@
namespace UsdLayerManager { namespace UsdLayerManager {
/// Serialisable snapshot of all per-tile render settings. /// Serialisable snapshot of per-tile render settings.
/// Color correction is global (AppPreferences); render delegate is per-tile.
struct ViewportTileSettings { struct ViewportTileSettings {
std::string renderDelegate; ///< TfToken string, empty = default (Storm) std::string renderDelegate; ///< empty = use AppPreferences default on first load
bool showGrid = true; bool showGrid = true;
bool showCameraGuide = false;
std::string aov = "color";
bool aaEnabled = false; bool aaEnabled = false;
float bgColorR = 0.15f; float bgColorR = 0.15f;
float bgColorG = 0.15f; float bgColorG = 0.15f;
float bgColorB = 0.15f; float bgColorB = 0.15f;
int bboxMode = 0; ///< BBoxMode cast to int int bboxMode = 0;
bool ambientLightOnly = true; bool ambientLightOnly = true;
bool domeLightEnabled = false; bool domeLightEnabled = false;
int shadingMode = 0; ///< ShadingMode cast to int (0 = SmoothShaded) int shadingMode = 0;
int colorCorrectionMode = 1; ///< ColorCorrectionMode cast to int (1 = sRGB) // CullStyle::Nothing — matches usdview's default (viewSettingsDataModel
std::string ocioDisplay; // cullBackfaces=False -> CULL_STYLE_NOTHING). BackUnlessDoubleSided drops
std::string ocioView; // back faces on geometry that isn't authored doubleSided, which hdEmbree
std::string ocioColorSpace; // applies to occlusion rays as well, so interiors shade as single-sided.
std::string ocioLook; int cullStyle = 1; // CullStyle::Nothing
bool showGuides = false;
bool showProxy = true;
bool showRender = false;
}; };
/// Named orthographic view directions. /// Named orthographic view directions.
@@ -183,16 +189,15 @@ private:
// ── Orthographic view ──────────────────────────────────────────────────── // ── Orthographic view ────────────────────────────────────────────────────
OrthoView m_orthoView = OrthoView::None; OrthoView m_orthoView = OrthoView::None;
// ── OCIO InputText edit buffers (per-tile, seeded on mode activation) ────
char m_ocioDisplayBuf[128] = {};
char m_ocioViewBuf[128] = {};
char m_ocioColorSpaceBuf[128] = {};
char m_ocioLookBuf[128] = {};
bool m_ocioFieldsSynced = false;
// ── 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