VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
asset.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 18.10.2025.
5//
6#include "asset.h"
7
8#define STB_IMAGE_IMPLEMENTATION
14#include "spdlog/spdlog.h"
15#include "stb_image.h"
16
17#define TINYGLTF_NO_STB_IMAGE
18#define TINYGLTF_NO_STB_IMAGE_WRITE
19#include <tiny_gltf.h>
20
21namespace visutwin::canvas
22{
23 std::weak_ptr<GraphicsDevice> Asset::_defaultGraphicsDevice;
24
25 Asset::Asset(const std::string& name, const std::string& type, const std::string& file,
26 const AssetData& data) : EventHandler(), _name(name), _type(type), _file(file), _data(data)
27 {
28 }
29
30 void Asset::setDefaultGraphicsDevice(const std::shared_ptr<GraphicsDevice>& graphicsDevice)
31 {
32 _defaultGraphicsDevice = graphicsDevice;
33 }
34
36 {
37 for (auto& resource : _resources) {
38 if (std::holds_alternative<Texture*>(resource)) {
39 delete std::get<Texture*>(resource);
40 } else if (std::holds_alternative<ContainerResource*>(resource)) {
41 delete std::get<ContainerResource*>(resource);
42 } else if (std::holds_alternative<FontResource*>(resource)) {
43 auto* font = std::get<FontResource*>(resource);
44 if (font) {
45 delete font->texture;
46 font->texture = nullptr;
47 delete font;
48 }
49 }
50 }
51 _resources.clear();
52 }
53
54 std::optional<Resource> Asset::resource() {
55 spdlog::info("Asset::resource loading '{}' type '{}'", _name, _type);
56 if (_resources.empty()) {
57 if (_type == AssetType::CONTAINER) {
58 auto graphicsDevice = _defaultGraphicsDevice.lock();
59 if (!graphicsDevice) {
60 spdlog::error("Cannot load container asset '{}': no graphics device set", _name);
61 return std::nullopt;
62 }
63
64 // Route by file extension: .obj → ObjParser, .stl → StlParser, else → GlbParser
65 const bool isObj = _file.size() >= 4 &&
66 (_file.compare(_file.size() - 4, 4, ".obj") == 0 ||
67 _file.compare(_file.size() - 4, 4, ".OBJ") == 0);
68
69 const bool isStl = _file.size() >= 4 &&
70 (_file.compare(_file.size() - 4, 4, ".stl") == 0 ||
71 _file.compare(_file.size() - 4, 4, ".STL") == 0);
72
73 const bool isAssimp = _file.size() >= 4 &&
74 (_file.compare(_file.size() - 4, 4, ".dae") == 0 ||
75 _file.compare(_file.size() - 4, 4, ".DAE") == 0 ||
76 _file.compare(_file.size() - 4, 4, ".fbx") == 0 ||
77 _file.compare(_file.size() - 4, 4, ".FBX") == 0 ||
78 _file.compare(_file.size() - 4, 4, ".3ds") == 0 ||
79 _file.compare(_file.size() - 4, 4, ".ply") == 0 ||
80 _file.compare(_file.size() - 4, 4, ".PLY") == 0);
81
82 std::unique_ptr<GlbContainerResource> container;
83 if (isObj) {
84 container = ObjParser::parse(_file, graphicsDevice);
85 } else if (isStl) {
86 container = StlParser::parse(_file, graphicsDevice);
87 } else if (isAssimp) {
88 container = AssimpParser::parse(_file, graphicsDevice);
89 } else {
90 container = GlbParser::parse(_file, graphicsDevice);
91 }
92 if (!container) {
93 return std::nullopt;
94 }
95 _resources.push_back(static_cast<ContainerResource*>(container.release()));
96 } else if (_type == AssetType::TEXTURE) {
97 auto graphicsDevice = _defaultGraphicsDevice.lock();
98 if (!graphicsDevice) {
99 spdlog::error("Cannot load texture asset '{}': no graphics device set", _name);
100 return std::nullopt;
101 }
102
103 int width = 0;
104 int height = 0;
105 int channels = 0;
106 // upstream texture loading keeps source orientation; env-atlas UV layout depends on this.
107 stbi_set_flip_vertically_on_load(false);
108
109 const bool isHdr = _file.size() >= 4 &&
110 _file.compare(_file.size() - 4, 4, ".hdr") == 0;
111
112 if (isHdr) {
113 // HDR path: load as floating-point data
114 float* hdrPixels = stbi_loadf(_file.c_str(), &width, &height, &channels, 0);
115 if (!hdrPixels || width <= 0 || height <= 0) {
116 spdlog::error("Failed to decode HDR texture asset '{}' from '{}'", _name, _file);
117 if (hdrPixels) {
118 stbi_image_free(hdrPixels);
119 }
120 return std::nullopt;
121 }
122
123 // Convert to RGBA float (stbi_loadf returns RGB for HDR)
124 const size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
125 std::vector<float> rgbaData(pixelCount * 4);
126 for (size_t i = 0; i < pixelCount; ++i) {
127 rgbaData[i * 4 + 0] = hdrPixels[i * channels + 0];
128 rgbaData[i * 4 + 1] = channels > 1 ? hdrPixels[i * channels + 1] : hdrPixels[i * channels + 0];
129 rgbaData[i * 4 + 2] = channels > 2 ? hdrPixels[i * channels + 2] : hdrPixels[i * channels + 0];
130 rgbaData[i * 4 + 3] = 1.0f;
131 }
132 stbi_image_free(hdrPixels);
133
134 TextureOptions options;
135 options.width = static_cast<uint32_t>(width);
136 options.height = static_cast<uint32_t>(height);
138 options.mipmaps = _data.mipmaps;
139 options.numLevels = _data.mipmaps ? 0 : 1;
140 options.name = _name;
141
142 auto* texture = new Texture(graphicsDevice.get(), options);
143 texture->setEncoding(TextureEncoding::Default);
144 const auto dataSize = pixelCount * 4 * sizeof(float);
145 texture->setLevelData(0, reinterpret_cast<const uint8_t*>(rgbaData.data()), dataSize);
146 texture->upload();
147
148 spdlog::info("Loaded HDR texture '{}': {}x{} channels={}", _name, width, height, channels);
149 _resources.push_back(texture);
150 } else {
151 // LDR path: load as 8-bit RGBA
152 stbi_uc* pixels = stbi_load(_file.c_str(), &width, &height, &channels, STBI_rgb_alpha);
153 if (!pixels || width <= 0 || height <= 0) {
154 spdlog::error("Failed to decode texture asset '{}' from '{}'", _name, _file);
155 if (pixels) {
156 stbi_image_free(pixels);
157 }
158 return std::nullopt;
159 }
160
161 TextureOptions options;
162 options.width = static_cast<uint32_t>(width);
163 options.height = static_cast<uint32_t>(height);
165 options.mipmaps = _data.mipmaps;
166 options.numLevels = _data.mipmaps ? 0 : 1;
167 options.name = _name;
168
169 auto* texture = new Texture(graphicsDevice.get(), options);
170 if (_data.type == TextureType::TEXTURETYPE_RGBP) {
171 texture->setEncoding(TextureEncoding::RGBP);
172 } else if (_data.type == TextureType::TEXTURETYPE_RGBM) {
173 texture->setEncoding(TextureEncoding::RGBM);
174 } else {
175 texture->setEncoding(TextureEncoding::Default);
176 }
177 const auto dataSize = static_cast<size_t>(width) * static_cast<size_t>(height) * 4u;
178 texture->setLevelData(0, reinterpret_cast<const uint8_t*>(pixels), dataSize);
179 texture->upload();
180
181 stbi_image_free(pixels);
182 _resources.push_back(texture);
183 }
184 } else if (_type == AssetType::FONT) {
185 auto graphicsDevice = _defaultGraphicsDevice.lock();
186 if (!graphicsDevice) {
187 spdlog::error("Cannot load font asset '{}': no graphics device set", _name);
188 return std::nullopt;
189 }
190
191 const auto font = loadBitmapFontResource(_file, graphicsDevice);
192 if (!font.has_value()) {
193 spdlog::error("Failed to decode bitmap font asset '{}' from '{}'", _name, _file);
194 return std::nullopt;
195 }
196 _resources.push_back(*font);
197 }
198 }
199
200 if (!_resources.empty()) {
201 return _resources[0];
202 }
203 return std::nullopt;
204 }
205
207 std::function<void(std::optional<Resource>)> callback)
208 {
209 // If already loaded, invoke callback immediately.
210 if (!_resources.empty()) {
211 spdlog::info("Asset::loadAsync '{}' already loaded — returning cached", _name);
212 if (callback) callback(_resources[0]);
213 return;
214 }
215
216 auto graphicsDevice = _defaultGraphicsDevice.lock();
217 if (!graphicsDevice) {
218 spdlog::error("Cannot async-load asset '{}': no graphics device set", _name);
219 if (callback) callback(std::nullopt);
220 return;
221 }
222
223 // Map asset type → handler type key used by ResourceLoader.
224 // (Asset types and handler keys deliberately share the same strings.)
225 const std::string& handlerType = _type;
226
227 // Capture fields by value so the closure is self-contained.
228 // `this` is captured for storing the result in _resources.
229 // SAFETY: the Asset must outlive the in-flight request.
230 auto* self = this;
231 auto name = _name;
232 auto type = _type;
233 auto file = _file;
234 auto data = _data;
235 auto device = graphicsDevice;
236
237 spdlog::info("Asset::loadAsync queuing '{}' type '{}'", _name, _type);
238
239 loader.load(_file, handlerType,
240 // ── onSuccess (main thread) ────────────────────────────────────
241 [self, name, type, file, data, device, callback](std::unique_ptr<LoadedData> loaded) {
242 if (type == AssetType::TEXTURE && loaded->pixelData) {
243 // GPU upload from pre-decoded pixels.
244 auto& pd = *loaded->pixelData;
245
246 TextureOptions options;
247 options.width = static_cast<uint32_t>(pd.width);
248 options.height = static_cast<uint32_t>(pd.height);
249 options.format = pd.isHdr ? PixelFormat::PIXELFORMAT_RGBA32F
251 options.mipmaps = data.mipmaps;
252 options.numLevels = data.mipmaps ? 0 : 1;
253 options.name = name;
254
255 auto* texture = new Texture(device.get(), options);
257 texture->setEncoding(TextureEncoding::RGBP);
258 } else if (data.type == TextureType::TEXTURETYPE_RGBM) {
259 texture->setEncoding(TextureEncoding::RGBM);
260 } else {
261 texture->setEncoding(TextureEncoding::Default);
262 }
263
264 if (pd.isHdr) {
265 texture->setLevelData(0,
266 reinterpret_cast<const uint8_t*>(pd.hdrPixels.data()),
267 pd.hdrPixels.size() * sizeof(float));
268 } else {
269 texture->setLevelData(0, pd.pixels.data(), pd.pixels.size());
270 }
271 texture->upload();
272
273 spdlog::info("Asset::loadAsync texture '{}' GPU upload done {}x{}",
274 name, pd.width, pd.height);
275
276 self->_resources.push_back(texture);
277 if (callback) callback(texture);
278
279 } else if (type == AssetType::CONTAINER) {
280 std::unique_ptr<GlbContainerResource> container;
281
282 // Fast path: if the background thread already did FULL
283 // CPU pre-processing (Draco, vertex extraction, tangent
284 // gen, pixel conversion, animation parse), only create
285 // GPU resources here — this is fast and avoids the
286 // main-thread stall that caused the spinning-wait cursor.
287 if (loaded->preparsed && loaded->preparedData) {
288 auto model = std::static_pointer_cast<tinygltf::Model>(loaded->preparsed);
289 auto prepared = std::static_pointer_cast<PreparedGlbData>(loaded->preparedData);
290 container = GlbParser::createFromPrepared(*model, std::move(*prepared), device, name);
291 } else if (loaded->preparsed) {
292 // Medium path: model pre-parsed but CPU work not done.
293 auto model = std::static_pointer_cast<tinygltf::Model>(loaded->preparsed);
294 container = GlbParser::createFromModel(*model, device, name);
295 } else {
296 // Slow fallback: pre-parse wasn't done (non-GLB format,
297 // or bg parse failed).
298 const bool isGlb = file.size() >= 4 &&
299 (file.compare(file.size() - 4, 4, ".glb") == 0 ||
300 file.compare(file.size() - 4, 4, ".GLB") == 0);
301
302 const bool isGltf = file.size() >= 5 &&
303 (file.compare(file.size() - 5, 5, ".gltf") == 0 ||
304 file.compare(file.size() - 5, 5, ".GLTF") == 0);
305
306 if ((isGlb || isGltf) && !loaded->bytes.empty()) {
307 container = GlbParser::parseFromMemory(
308 loaded->bytes.data(), loaded->bytes.size(), device, name);
309 } else {
310 // OBJ / STL / Assimp — these parsers need file
311 // paths (their libraries read from disk directly).
312 // The bg thread pre-read the bytes to warm cache.
313 const bool isObj = file.size() >= 4 &&
314 (file.compare(file.size() - 4, 4, ".obj") == 0 ||
315 file.compare(file.size() - 4, 4, ".OBJ") == 0);
316
317 const bool isStl = file.size() >= 4 &&
318 (file.compare(file.size() - 4, 4, ".stl") == 0 ||
319 file.compare(file.size() - 4, 4, ".STL") == 0);
320
321 const bool isAssimp = file.size() >= 4 &&
322 (file.compare(file.size() - 4, 4, ".dae") == 0 ||
323 file.compare(file.size() - 4, 4, ".DAE") == 0 ||
324 file.compare(file.size() - 4, 4, ".fbx") == 0 ||
325 file.compare(file.size() - 4, 4, ".FBX") == 0 ||
326 file.compare(file.size() - 4, 4, ".3ds") == 0 ||
327 file.compare(file.size() - 4, 4, ".ply") == 0 ||
328 file.compare(file.size() - 4, 4, ".PLY") == 0);
329
330 if (isObj) {
331 container = ObjParser::parse(file, device);
332 } else if (isStl) {
333 container = StlParser::parse(file, device);
334 } else if (isAssimp) {
335 container = AssimpParser::parse(file, device);
336 } else {
337 // Fallback: try GlbParser from memory.
338 if (!loaded->bytes.empty()) {
339 container = GlbParser::parseFromMemory(
340 loaded->bytes.data(), loaded->bytes.size(),
341 device, name);
342 }
343 }
344 }
345 }
346
347 if (container) {
348 auto* res = static_cast<ContainerResource*>(container.release());
349 self->_resources.push_back(res);
350 spdlog::info("Asset::loadAsync container '{}' parse done", name);
351 if (callback) callback(res);
352 } else {
353 spdlog::error("Asset::loadAsync container '{}' parse failed", name);
354 if (callback) callback(std::nullopt);
355 }
356
357 } else if (type == AssetType::FONT) {
358 // Font parsing still uses the file path (loadBitmapFontResource).
359 // The bg thread pre-read the bytes to warm the OS cache.
360 const auto font = loadBitmapFontResource(file, device);
361 if (font.has_value()) {
362 self->_resources.push_back(*font);
363 spdlog::info("Asset::loadAsync font '{}' parse done", name);
364 if (callback) callback(*font);
365 } else {
366 spdlog::error("Asset::loadAsync font '{}' parse failed", name);
367 if (callback) callback(std::nullopt);
368 }
369
370 } else {
371 spdlog::warn("Asset::loadAsync '{}' unsupported type '{}'", name, type);
372 if (callback) callback(std::nullopt);
373 }
374 },
375 // ── onError (main thread) ──────────────────────────────────────
376 [name, callback](const std::string& error) {
377 spdlog::error("Asset::loadAsync '{}' failed: {}", name, error);
378 if (callback) callback(std::nullopt);
379 }
380 );
381 }
382}
static void setDefaultGraphicsDevice(const std::shared_ptr< GraphicsDevice > &graphicsDevice)
Definition asset.cpp:30
const AssetData & data() const
Definition asset.h:82
void loadAsync(ResourceLoader &loader, std::function< void(std::optional< Resource >)> callback)
Definition asset.cpp:206
std::optional< Resource > resource()
Synchronous load — blocks until the resource is ready.
Definition asset.cpp:54
const std::string & type() const
Definition asset.h:80
Asset(const std::string &name, const std::string &type, const std::string &file, const AssetData &data={})
Definition asset.cpp:25
const std::string & file() const
Definition asset.h:81
const std::string & name() const
Definition asset.h:79
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device, const AssimpParserConfig &config=AssimpParserConfig{})
static std::unique_ptr< GlbContainerResource > createFromModel(tinygltf::Model &model, const std::shared_ptr< GraphicsDevice > &device, const std::string &debugName="memory")
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device)
Parse a GLB file from disk.
static std::unique_ptr< GlbContainerResource > createFromPrepared(tinygltf::Model &model, PreparedGlbData &&prepared, const std::shared_ptr< GraphicsDevice > &device, const std::string &debugName="memory")
static std::unique_ptr< GlbContainerResource > parseFromMemory(const std::uint8_t *data, std::size_t length, const std::shared_ptr< GraphicsDevice > &device, const std::string &debugName="memory")
Parse a GLB from an in-memory byte buffer (e.g. extracted from b3dm).
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device, const ObjParserConfig &config=ObjParserConfig{})
Async resource loader with background I/O thread, pixel decoding, and main-thread callbacks.
void load(const std::string &url, const std::string &type, LoadSuccessCallback onSuccess, LoadErrorCallback onError=nullptr)
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device, const StlParserConfig &config=StlParserConfig{})
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
constexpr const char * CONTAINER
Definition asset.h:25
constexpr const char * FONT
Definition asset.h:28
constexpr const char * TEXTURE
Definition asset.h:40
constexpr const char * TEXTURETYPE_RGBM
Definition asset.h:48
constexpr const char * TEXTURETYPE_RGBP
Definition asset.h:50
std::optional< FontResource * > loadBitmapFontResource(const std::string &jsonPath, const std::shared_ptr< GraphicsDevice > &graphicsDevice)