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