Loading...
Searching...
No Matches
MaterialOverride.hpp
1#pragma once
2#include <halp/controls.hpp>
3#include <halp/meta.hpp>
4#include <halp/texture.hpp>
5
6#include <ossia/dataflow/geometry_port.hpp>
7#include <ossia/detail/hash_map.hpp>
8
9#include <cstdint>
10#include <memory>
11#include <vector>
12
13namespace Threedim
14{
15
16// Injects runtime GPU textures and/or factor overrides into a scene's
17// material table. The primary live-VJ use case: drop a video texture
18// (or HDR shader output) onto an existing material without reloading
19// the scene. Authored on top of the Dynamic Texture pathway in
20// ScenePreprocessor — the texture handle is forwarded verbatim and
21// ScenePreprocessor emits it as a `*Dyn<slot>` auxiliary-texture binding
22// that classic_pbr_full (and any shader opting into the DYNAMIC source
23// branch) samples directly.
24//
25// Scope: the four PBR slots (base color / metal-rough / normal /
26// emissive). Occlusion and extension textures (transmission, clearcoat,
27// sheen…) are not in the ScenePreprocessor's array pool yet, so
28// overriding them here would have no effect downstream.
29//
30// Mode:
31// All — every material in the scene gets the override applied.
32// ByIndex — only `scene.state->materials[Index]` is overridden. Other
33// materials pass through unchanged. Use Scene Inspector +
34// the ByIndex variant to target a single object.
35//
36// Factor toggles gate whether the scalar/vector controls take effect;
37// textures auto-gate on "handle is non-null" so an unwired inlet is a
38// no-op regardless of state.
40{
41public:
42 halp_meta(name, "Material Override")
43 halp_meta(category, "Visuals/3D/Scene")
44 halp_meta(c_name, "material_override")
45 halp_meta(authors, "ossia team")
46 halp_meta(
47 manual_url,
48 "https://ossia.io/score-docs/processes/material-override.html")
49 halp_meta(uuid, "c3d8e5f2-9a4b-4e7d-b8c1-2f6a9e3d5b7c")
50
51 enum Mode
52 {
53 All,
54 ByIndex
55 };
56
57 struct ins
58 {
59 struct
60 {
61 halp_meta(name, "Scene In");
62 ossia::scene_spec scene;
63 uint8_t dirty{0};
64 } scene_in;
65
66 // Port-driven rebuild: scalar controls trigger rebuild() via their
67 // update() callbacks. Texture handles are checked in operator()()
68 // because their native handles can change without a port-update
69 // event (live video / CSF outputs swap native handles mid-stream).
70 struct : halp::combobox_t<"Mode", Mode>
71 {
72 struct range
73 {
74 std::string_view values[2]{"All", "By Index"};
75 int init{0};
76 };
77 void update(MaterialOverride& n) { n.rebuild(); }
78 } mode;
79 struct : halp::spinbox_i32<"Index", halp::irange{0, 4096, 0}>
80 { void update(MaterialOverride& n) { n.rebuild(); } } index;
81
82 // Texture overrides. Unwired (handle==nullptr) → pass through.
83 // Handle changes detected in operator()(), not via control update().
84 halp::gpu_texture_input<"Base Color Tex"> base_color_tex;
85 halp::gpu_texture_input<"Metal Rough Tex"> metal_rough_tex;
86 halp::gpu_texture_input<"Normal Tex"> normal_tex;
87 halp::gpu_texture_input<"Emissive Tex"> emissive_tex;
88
89 // Factor overrides. Gated on the companion toggles; otherwise the
90 // original factor from the loader passes through.
91 struct : halp::toggle<"Use base color">
92 { void update(MaterialOverride& n) { n.rebuild(); } } use_base_color;
93 struct : halp::hslider_f32<"R", halp::range{0., 1., 1.}>
94 { void update(MaterialOverride& n) { n.rebuild(); } } base_r;
95 struct : halp::hslider_f32<"G", halp::range{0., 1., 1.}>
96 { void update(MaterialOverride& n) { n.rebuild(); } } base_g;
97 struct : halp::hslider_f32<"B", halp::range{0., 1., 1.}>
98 { void update(MaterialOverride& n) { n.rebuild(); } } base_b;
99 struct : halp::hslider_f32<"A", halp::range{0., 1., 1.}>
100 { void update(MaterialOverride& n) { n.rebuild(); } } base_a;
101
102 struct : halp::toggle<"Use metallic">
103 { void update(MaterialOverride& n) { n.rebuild(); } } use_metallic;
104 struct : halp::hslider_f32<"Metallic", halp::range{0., 1., 0.}>
105 { void update(MaterialOverride& n) { n.rebuild(); } } metallic;
106
107 struct : halp::toggle<"Use roughness">
108 { void update(MaterialOverride& n) { n.rebuild(); } } use_roughness;
109 struct : halp::hslider_f32<"Roughness", halp::range{0., 1., 0.5}>
110 { void update(MaterialOverride& n) { n.rebuild(); } } roughness;
111
112 struct : halp::toggle<"Use emissive">
113 { void update(MaterialOverride& n) { n.rebuild(); } } use_emissive;
114 struct : halp::hslider_f32<"Emissive R", halp::range{0., 10., 0.}>
115 { void update(MaterialOverride& n) { n.rebuild(); } } em_r;
116 struct : halp::hslider_f32<"Emissive G", halp::range{0., 10., 0.}>
117 { void update(MaterialOverride& n) { n.rebuild(); } } em_g;
118 struct : halp::hslider_f32<"Emissive B", halp::range{0., 10., 0.}>
119 { void update(MaterialOverride& n) { n.rebuild(); } } em_b;
120 struct : halp::hslider_f32<"Emissive strength", halp::range{0., 10., 1.}>
121 { void update(MaterialOverride& n) { n.rebuild(); } } em_strength;
122 } inputs;
123
124 struct outs
125 {
126 struct
127 {
128 halp_meta(name, "Scene Out");
129 ossia::scene_spec scene;
130 uint8_t dirty{0};
131 } scene_out;
132 } outputs;
133
134 void rebuild();
135 void operator()();
136
137 // Cached output; stable shared_ptr identity when inputs haven't
138 // changed so ScenePreprocessor's per-frame fingerprint fast-path stays
139 // warm. Dynamic-texture swaps still propagate because ScenePreprocessor
140 // refreshes its dynamic-slot map every frame (keyed on native_handle).
141 std::shared_ptr<const ossia::scene_state> m_cached_out;
142 uint8_t m_pending_dirty{0xFF};
143
144 // Cache of override clones keyed by source material_component*. We
145 // reuse the same std::shared_ptr<ossia::material_component> clone
146 // across rebuilds when the source is unchanged, MUTATING its fields
147 // in place. That keeps the shared_ptr address stable → the
148 // preprocessor's m_loaderMaterialSlots keeps the arena slot allocated
149 // across frames → no per-frame GC + re-allocate cycle → the Material
150 // arena SSBO content is stable without churn. When the upstream
151 // material list changes structurally, stale cache entries are
152 // garbage-collected in rebuild().
153 ossia::hash_map<
154 const ossia::material_component*,
155 std::shared_ptr<ossia::material_component>>
156 m_clone_cache;
157
158 // Identity cache: (input-scene pointer, input version, control values,
159 // texture handles). If all match, we reuse m_cached_out without
160 // rebuilding the materials list.
161 const ossia::scene_state* m_cached_in_state{};
162 // -2 is a sentinel no live input can produce (a null input reads as -1):
163 // it forces the first tick's rebuild without needing `!m_cached_out` as a
164 // trigger — a null cached output is legitimate whenever the input scene
165 // is null, and using it as a rebuild trigger dirties every tick.
166 int64_t m_cached_in_version{-2};
167 int m_cached_mode{-1};
168 int m_cached_index{-1};
169 void* m_cached_tex[4]{};
170 bool m_cached_use_base{false};
171 bool m_cached_use_metallic{false};
172 bool m_cached_use_roughness{false};
173 bool m_cached_use_emissive{false};
174 float m_cached_base[4]{};
175 float m_cached_metallic{-1.f};
176 float m_cached_roughness{-1.f};
177 float m_cached_em[4]{};
178 int64_t m_version_counter{0};
179};
180
181}
Definition MaterialOverride.hpp:40
Definition MaterialOverride.hpp:58
Definition MaterialOverride.hpp:125
Definition MIDISync.hpp:126