Loading...
Searching...
No Matches
GltfLiveLink.hpp
1#pragma once
2#include <Threedim/LiveLink/NativeScene.hpp>
3#include <Threedim/LiveLink/Server.hpp>
4#include <Threedim/TinyObj.hpp>
5#include <Threedim/TransformHelper.hpp>
6#include <halp/controls.hpp>
7#include <halp/meta.hpp>
8
9#include <ossia/dataflow/geometry_port.hpp>
10
11#include <Gfx/Graph/GpuResourceRegistry.hpp>
12
13#include <cstdint>
14#include <functional>
15#include <map>
16#include <memory>
17#include <optional>
18#include <string>
19#include <unordered_map>
20#include <variant>
21#include <vector>
22
23class QRhiResourceUpdateBatch;
24
25namespace score::gfx
26{
27class RenderList;
28struct Edge;
29}
30
31namespace Threedim
32{
33
34// Receives a scene streamed live from a DCC -- the Blender add-on,
35// https://github.com/ossia/score-blender-livelink -- and publishes it like
36// Asset Loader does.
37//
38// The node listens on a TCP port (protocol: LiveLink/Protocol.hpp). The sender
39// pushes the scene when its structure changes: a complete binary glTF, a glTF
40// patch of a few objects, removals, or native frames (vertex buffers, images
41// and a JSON scene, LiveLink/NativeScene.hpp). Parsing a glTF and generating a
42// native mesh's tangents happen on the worker, so the render thread never
43// waits for them; changes are applied in the order they were sent.
44//
45// Everything else arrives as values that override the last scene, applied in
46// one structure-sharing clone of the tree (as AnimationPlayer does for sampled
47// channels), so a change costs no reparse:
48//
49// - object transforms, by glTF node name: the first scene_transform of the
50// node is replaced;
51// - bone poses, by armature and bone name: the bone node's transform and the
52// skeleton joint the renderer skins with;
53// - morph target (shape key) weights: the node's mesh is re-blended on the
54// CPU (MorphTargets.hpp), since the renderer does not morph;
55// - visibility: scene_node::visible;
56// - light and camera parameters: the node's component is replaced;
57// - material factors, by material name: the material is replaced, in
58// scene_state::materials and in every primitive that uses it.
59//
60// The scene keeps being published after the DCC disconnects; a reconnecting
61// sender sends a full scene first.
62//
63// Position / Rotation / Scale wrap the received scene at a root TRS, exactly
64// as for Asset Loader.
66{
67public:
68 halp_meta(name, "glTF Live Link")
69 halp_meta(category, "Visuals/3D")
70 halp_meta(c_name, "gltf_live_link")
71 halp_meta(
72 description,
73 "Receive a 3D scene streamed live from Blender: geometry and materials "
74 "as glTF, object transforms as they move.")
75 halp_meta(authors, "ossia team")
76 halp_meta(
77 manual_url, "https://ossia.io/score-docs/processes/gltf-live-link.html")
78 halp_meta(uuid, "b9446bac-dbeb-4672-a9b6-db6e6cdaef2e")
79
80 struct ins
81 {
82 halp::spinbox_i32<"Port", halp::irange{1, 65535, LiveLink::default_port}> port;
83 // Loopback by default: anything on the network that can reach the port
84 // can replace the scene. 0.0.0.0 accepts a DCC on another machine.
85 halp::lineedit<"Bind address", "127.0.0.1"> bind;
86 halp::toggle<"Listen", halp::toggle_setup{.init = true}> listen;
87
88 PositionControl position;
89 RotationControl rotation;
90 ScaleControl scale;
91
92 // What of the received scene is used. Off, the renderer falls back to
93 // what the rest of the score graph provides: its own camera and lights,
94 // the default material, unskinned meshes in their bind pose.
95 halp::toggle<"Cameras", halp::toggle_setup{.init = true}> cameras;
96 halp::toggle<"Lights", halp::toggle_setup{.init = true}> lights;
97 halp::toggle<"Materials", halp::toggle_setup{.init = true}> materials;
98 halp::toggle<"Skinning", halp::toggle_setup{.init = true}> skinning;
99 // Streamed values: off keeps what the last exported scene holds.
100 halp::toggle<"Bone poses", halp::toggle_setup{.init = true}> bone_poses;
101 halp::toggle<"Shape keys", halp::toggle_setup{.init = true}> shape_keys;
102 halp::toggle<"World", halp::toggle_setup{.init = true}> world;
103 // Position / Rotation / Scale place the scene in front of the cameras;
104 // on, the cameras move with it (and the view does not change).
105 halp::toggle<"Move cameras", halp::toggle_setup{.init = false}> move_cameras;
106 } inputs;
107
108 struct outs
109 {
110 struct
111 {
112 halp_meta(name, "Scene");
113 ossia::scene_spec scene;
114 uint8_t dirty{0};
115 } scene_out;
116 } outputs;
117
118 GltfLiveLink();
120
121 void operator()();
122
123 void init(score::gfx::RenderList& r, QRhiResourceUpdateBatch& res);
124 void update(
125 score::gfx::RenderList& r, QRhiResourceUpdateBatch& res,
127 void release(score::gfx::RenderList& r);
128
129 // Parses a received .glb. GltfParser loads from a path, so the bytes are
130 // written to a per-port file first: the same path on every update keeps
131 // the parser's path-derived material ids, and with them downstream caches,
132 // stable across updates.
133 // A patch (a few objects, added to the scene) is parsed the same way.
134 // A native mesh gets its tangents there too: MikkTSpace on a large mesh
135 // takes seconds, which the server's thread, reading the sender's frames,
136 // cannot spend without the sender timing out.
137 struct glb_job
138 {
139 std::string glb;
140 std::string path;
141 uint64_t seq{};
142 bool patch{};
143 };
144 // Shared: the task pool's jobs are small.
145 struct mesh_job
146 {
147 std::shared_ptr<LiveLink::structural_message> message;
148 };
149 using job = std::variant<glb_job, mesh_job>;
150 struct worker
151 {
152 std::function<void(job)> request;
153
154 static std::function<void(GltfLiveLink&)> work(job j);
155 } worker;
156
157 // Applies a parsed scene. `material_names` parallels parsed->materials.
158 // Public for the worker's closure and the tests.
159 void set_scene(
160 std::shared_ptr<const ossia::scene_state> parsed, uint64_t seq,
161 std::vector<std::string> material_names = {});
162
163 // A parse finished (state is null when it failed).
164 void on_parsed(
165 bool patch, uint64_t seq, std::shared_ptr<const ossia::scene_state> state,
166 std::vector<std::string> material_names);
167
168 // A native mesh's tangents are generated.
169 void on_mesh_ready(LiveLink::structural_message m);
170
171 // Adds a parsed patch's root nodes, materials and skeletons to the scene,
172 // its node ids moved past the scene's.
173 void add_patch(
174 std::shared_ptr<const ossia::scene_state> patch, uint64_t seq,
175 std::vector<std::string> material_names = {});
176
177 // Removes the named nodes, their subtrees, and the materials left unused.
178 void remove_nodes(const std::vector<std::string>& names, uint64_t seq);
179
180 // Applies received values (and requests the parse of a received scene).
181 // Public for the tests, which drive the node without a socket.
182 void apply(LiveLink::inbox box);
183 void set_transform(const LiveLink::transform_record& r, uint64_t seq);
184
185 // Raw scene as parsed, then with the streamed values applied, then
186 // wrapped in the Position / Rotation / Scale controls (published).
187 std::shared_ptr<const ossia::scene_state> m_parsed_state;
188 std::shared_ptr<const ossia::scene_state> m_overridden_state;
189 std::shared_ptr<const ossia::scene_state> m_wrapped_state;
190
191 // What the streamed values derived from m_parsed_state on the last
192 // rebuild, with what each came from: the source component and the seq of
193 // the record applied to it. The next rebuild hands back the same pointer
194 // for an unchanged pair, so that a change to one material, light or shape
195 // key does not make every other overridden one look new to the renderer
196 // (its material and light slots and its mesh uploads are keyed by
197 // pointer), nor re-blend every morphed mesh.
198 template <typename Ptr>
199 struct derived
200 {
201 const void* source{};
202 uint64_t seq{};
203 Ptr result;
204 };
206 {
207 uint64_t weights_seq{};
208 std::vector<const ossia::material_component*> materials;
209 ossia::mesh_component_ptr result;
210 };
212 {
213 // By scene_node id; materials by index in scene_state::materials.
214 using mesh_key = std::pair<uint64_t, const ossia::mesh_component*>;
215 std::unordered_map<uint64_t, derived<ossia::light_component_ptr>> lights;
216 std::unordered_map<uint64_t, derived<ossia::camera_component_ptr>> cameras;
217 std::unordered_map<std::size_t, derived<ossia::material_component_ptr>> materials;
218 std::map<mesh_key, derived_mesh> meshes;
219 } m_derived;
220
221private:
222 void restart_server();
223 void request(job j);
224 void apply_pending();
225 void rebuild_lookups();
226 void receive();
227 void rebuild_overridden_state();
228 void apply_overrides();
229 std::shared_ptr<const ossia::scene_state>
230 filter_components(std::shared_ptr<const ossia::scene_state> st);
231 void rebuild_wrapped_state();
232
233 std::unique_ptr<LiveLink::server> m_server;
234 std::string m_server_bind;
235 int m_server_port{-1};
236 bool m_server_failed{};
237 int m_retry_countdown{};
238
239 // Seq of the scene in m_parsed_state, and of the newest one handed to the
240 // worker: a parse that finishes after a newer one was requested is dropped.
241 uint64_t m_scene_seq{};
242 uint64_t m_requested_scene_seq{};
243
244 // Scenes, patches and removals received, waiting to be applied in seq
245 // order: the first ones still being parsed hold back those after them.
246 struct pending_change
247 {
248 uint64_t seq{};
249 enum kind_t : uint8_t
250 {
251 scene,
252 patch,
253 remove,
254 native // a mesh, an image or a native scene: `message`
255 } kind{};
256 bool ready{};
257 std::shared_ptr<const ossia::scene_state> state;
258 // Material names of a parsed scene or patch, node names of a removal.
259 std::vector<std::string> names;
260 LiveLink::structural_message message;
261
262 // Older meshes and images are not made obsolete by a newer scene: the
263 // native scene sent after them refers to them.
264 bool is_resource() const noexcept { return kind == native && message.is_resource(); }
265 };
266 std::vector<pending_change> m_pending;
267 LiveLink::native_resources m_native;
268 void apply_native(LiveLink::structural_message& m);
269
270 // Parallel to m_parsed_state->materials.
271 std::vector<std::string> m_material_names;
272 uint64_t m_max_node_id{};
273 std::optional<LiveLink::stamped<LiveLink::environment_record>> m_environment;
274 std::optional<LiveLink::stamped<LiveLink::node_name_record>> m_active_camera;
275
276 // Streamed values, newest per key. Each keeps the seq it came with, so
277 // that a new scene drops exactly those it already contains.
278 LiveLink::latest<LiveLink::transform_record> m_transforms;
279 LiveLink::latest<LiveLink::joint_record> m_joints;
280 LiveLink::latest<LiveLink::weights_record> m_weights;
281 LiveLink::latest<LiveLink::visibility_record> m_visibility;
282 LiveLink::latest<LiveLink::light_record> m_lights;
283 LiveLink::latest<LiveLink::camera_record> m_cameras;
284 LiveLink::latest<LiveLink::material_record> m_materials;
285
286 // `dirty` is the scene_port flag the value's change raises.
287 template <typename Record>
288 void store(
289 LiveLink::latest<Record>& map, std::string key, uint64_t seq, Record r,
290 uint8_t dirty);
291
292 // Name lookups into m_parsed_state: glTF node name -> scene_node ids, a
293 // bone (LiveLink::joint_key of any ancestor's name and the bone's name) ->
294 // scene_node id, material name -> its indices in scene_state::materials (a
295 // patch brings its own copies of the materials it uses).
296 std::unordered_multimap<std::string, uint64_t> m_ids_by_name;
297 std::unordered_map<std::string, uint64_t> m_ids_by_joint;
298 std::unordered_multimap<std::string, std::size_t> m_material_index;
299 bool m_overrides_dirty{};
300 uint8_t m_overrides_dirt{};
301 bool m_scene_dirty{};
302
303 CachedTRS m_cached_xform;
304
305 // The toggles the published scene was built with.
306 struct import_options
307 {
308 bool cameras, lights, materials, skinning, bone_poses, shape_keys, world,
309 move_cameras;
310 bool operator==(const import_options&) const noexcept = default;
311 };
312 import_options current_options() const noexcept;
313 std::optional<import_options> m_options;
314 // Meshes without their materials (Materials off) or their skin (Skinning
315 // off), by source mesh: kept so that the same mesh comes out as the same
316 // pointer on every rebuild. Emptied when the toggles change.
317 std::unordered_map<const ossia::mesh_component*, ossia::mesh_component_ptr>
318 m_stripped;
319 int64_t m_version_counter{0};
320
321 score::gfx::GpuResourceRegistry::Slot raw_transform_slot;
322 ossia::gpu_slot_ref m_xform_ref{};
323};
324
325}
List of nodes to be rendered to an output.
Definition RenderList.hpp:36
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
STL namespace.
Definition TinyObj.hpp:98
Definition TinyObj.hpp:103
Definition TinyObj.hpp:107
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236
Definition GpuResourceRegistry.hpp:84