1 // Copyright 2009-2021 Intel Corporation
2 // SPDX-License-Identifier: Apache-2.0
3
4 #include "Texture2D.h"
5 #include <sstream>
6 #include "rkcommon/memory/malloc.h"
7
8 #include "stb_image.h"
9
10 namespace ospray {
11 namespace sg {
12
13 // static helper functions //////////////////////////////////////////////////
14
15 //
16 // Generic
17 //
18 // Create the childData node given all other texture params
createDataNode()19 void Texture2D::createDataNode()
20 {
21 if (params.depth == 1)
22 createDataNodeType_internal<uint8_t>();
23 else if (params.depth == 2)
24 createDataNodeType_internal<uint16_t>();
25 else if (params.depth == 4)
26 createDataNodeType_internal<float>();
27 else
28 std::cerr << "#osp:sg: INVALID Texture depth " << params.depth << std::endl;
29 }
30
31 template <typename T>
createDataNodeType_internal()32 void Texture2D::createDataNodeType_internal()
33 {
34 if (params.components == 1)
35 createDataNodeVec_internal<T>();
36 else if (params.components == 2)
37 createDataNodeVec_internal<T, 2>();
38 else if (params.components == 3)
39 createDataNodeVec_internal<T, 3>();
40 else if (params.components == 4)
41 createDataNodeVec_internal<T, 4>();
42 else
43 std::cerr << "#osp:sg: INVALID number of texture components "
44 << params.components << std::endl;
45 }
46 template <typename T, int N>
createDataNodeVec_internal()47 void Texture2D::createDataNodeVec_internal()
48 {
49 using vecT = vec_t<T, N>;
50 // If texture doesn't use all channels(4), setup a strided-data access
51 if (params.colorChannel < 4) {
52 createChildData("data",
53 params.size, // numItems
54 sizeof(vecT) * vec2ul(1, params.size.x), // byteStride
55 (T *)texelData.get() + params.colorChannel,
56 true);
57 } else // RGBA
58 createChildData(
59 "data", params.size, vec2ul(0, 0), (vecT *)texelData.get(), true);
60 }
61 template <typename T>
createDataNodeVec_internal()62 void Texture2D::createDataNodeVec_internal()
63 {
64 createChildData(
65 "data", params.size, vec2ul(0, 0), (T *)texelData.get(), true);
66 }
67
osprayTextureFormat(int components)68 OSPTextureFormat Texture2D::osprayTextureFormat(int components)
69 {
70 if (params.depth == 1) {
71 if (components == 1)
72 return params.preferLinear ? OSP_TEXTURE_R8 : OSP_TEXTURE_L8;
73 if (components == 2)
74 return params.preferLinear ? OSP_TEXTURE_RA8 : OSP_TEXTURE_LA8;
75 if (components == 3)
76 return params.preferLinear ? OSP_TEXTURE_RGB8 : OSP_TEXTURE_SRGB;
77 if (components == 4)
78 return params.preferLinear ? OSP_TEXTURE_RGBA8 : OSP_TEXTURE_SRGBA;
79 } else if (params.depth == 2) {
80 if (components == 1)
81 return OSP_TEXTURE_R16;
82 if (components == 2)
83 return OSP_TEXTURE_RA16;
84 if (components == 3)
85 return OSP_TEXTURE_RGB16;
86 if (components == 4)
87 return OSP_TEXTURE_RGBA16;
88 } else if (params.depth == 4) {
89 if (components == 1)
90 return OSP_TEXTURE_R32F;
91 if (components == 3)
92 return OSP_TEXTURE_RGB32F;
93 if (components == 4)
94 return OSP_TEXTURE_RGBA32F;
95 }
96
97 std::cerr << "#osp:sg: INVALID format " << params.depth << ":"
98 << components << std::endl;
99 return OSP_TEXTURE_FORMAT_INVALID;
100 }
101
102 #ifdef USE_OPENIMAGEIO
103 //
104 // OpenImageIO
105 //
106 OIIO_NAMESPACE_USING
107 template <typename T>
loadTexture_OIIO_readFile(std::unique_ptr<ImageInput> & in)108 void Texture2D::loadTexture_OIIO_readFile(std::unique_ptr<ImageInput> &in)
109 {
110 const ImageSpec &spec = in->spec();
111 const auto typeDesc = TypeDescFromC<T>::value();
112
113 std::shared_ptr<void> data(new T[params.size.product() * params.components]);
114 T *start = (T *)data.get()
115 + (params.flip ? (params.size.y - 1) * params.size.x * params.components
116 : 0);
117 const long int stride =
118 (params.flip ? -1 : 1) * params.size.x * sizeof(T) * params.components;
119
120 bool success =
121 in->read_image(typeDesc, start, AutoStride, stride, AutoStride);
122 if (success) {
123 // Move shared_ptr ownership
124 texelData = data;
125 }
126 }
127
loadTexture_OIIO(const std::string & fileName)128 void Texture2D::loadTexture_OIIO(const std::string &fileName)
129 {
130 auto in = ImageInput::open(fileName.c_str());
131 if (in) {
132 const ImageSpec &spec = in->spec();
133 const auto typeDesc = spec.format.elementtype();
134
135 params.size = vec2ul(spec.width, spec.height);
136 params.components = spec.nchannels;
137 params.depth = spec.format.size();
138
139 if (params.depth == 1)
140 loadTexture_OIIO_readFile<uint8_t>(in);
141 else if (params.depth == 2 && (typeDesc != TypeDesc::FLOAT))
142 loadTexture_OIIO_readFile<uint16_t>(in);
143 else if (params.depth == 4)
144 loadTexture_OIIO_readFile<float>(in);
145 else
146 std::cerr << "#osp:sg: INVALID Texture depth " << params.depth
147 << std::endl;
148
149 in->close();
150 #if OIIO_VERSION < 10903 && OIIO_VERSION > 10603
151 ImageInput::destroy(in);
152 #endif
153 }
154
155 if (!texelData.get()) {
156 std::cerr << "#osp:sg: OpenImageIO failed to load texture '" << fileName
157 << "'" << std::endl;
158 }
159 }
160
161 #else
162 //
163 // PFM
164 //
loadTexture_PFM_readFile(FILE * file,float scaleFactor)165 void Texture2D::loadTexture_PFM_readFile(FILE *file, float scaleFactor)
166 {
167 size_t size = params.size.product() * params.components;
168 std::shared_ptr<void> data(new float[size]);
169 const size_t dataSize = sizeof(size) * sizeof(float);
170
171 int rc = fread(data.get(), dataSize, 1, file);
172 if (rc) {
173 // Scale texels by scale factor
174 float *texels = (float *)data.get();
175 for (size_t i = 0; i < params.size.product(); i++)
176 texels[i] *= scaleFactor;
177
178 // Move shared_ptr ownership
179 texelData = data;
180 }
181 }
182
loadTexture_PFM(const std::string & fileName)183 void Texture2D::loadTexture_PFM(const std::string &fileName)
184 {
185 FILE *file = nullptr;
186 try {
187 // Note: the PFM file specification does not support comments thus we
188 // don't skip any http://netpbm.sourceforge.net/doc/pfm.html
189 int rc = 0;
190 file = fopen(fileName.c_str(), "rb");
191 if (!file) {
192 throw std::runtime_error(
193 "#ospray_sg: could not open texture file '" + fileName + "'.");
194 }
195 // read format specifier:
196 // PF: color floating point image
197 // Pf: grayscale floating point image
198 char format[2] = {0};
199 if (fscanf(file, "%c%c\n", &format[0], &format[1]) != 2)
200 throw std::runtime_error("could not fscanf");
201
202 if (format[0] != 'P' || (format[1] != 'F' && format[1] != 'f')) {
203 throw std::runtime_error(
204 "#ospray_sg: invalid pfm texture file, header is not PF or "
205 "Pf");
206 }
207
208 params.components = 3;
209 if (format[1] == 'f') {
210 params.components = 1;
211 }
212
213 // read width and height
214 int width = -1;
215 int height = -1;
216 rc = fscanf(file, "%i %i\n", &width, &height);
217 if (rc != 2 || width < 0 || height < 0) {
218 throw std::runtime_error(
219 "#ospray_sg: could not parse width and height in PF PFM file "
220 "'" +
221 fileName +
222 "'. "
223 "Please report this bug at ospray.github.io, and include named "
224 "file to reproduce the error.");
225 }
226
227 // read scale factor/endiannes
228 float scaleEndian = 0.0;
229 rc = fscanf(file, "%f\n", &scaleEndian);
230
231 if (rc != 1) {
232 throw std::runtime_error(
233 "#ospray_sg: could not parse scale factor/endianness in PF "
234 "PFM file '" +
235 fileName +
236 "'. "
237 "Please report this bug at ospray.github.io, and include named "
238 "file to reproduce the error.");
239 }
240 if (scaleEndian == 0.0) {
241 throw std::runtime_error(
242 "#ospray_sg: scale factor/endianness in PF PFM file can not be 0");
243 }
244 if (scaleEndian > 0.0) {
245 throw std::runtime_error(
246 "#ospray_sg: could not parse PF PFM file '" + fileName +
247 "': currently supporting only little endian formats"
248 "Please report this bug at ospray.github.io, and include named "
249 "file to reproduce the error.");
250 }
251
252 float scaleFactor = std::abs(scaleEndian);
253 params.size = vec2ul(width, height);
254 params.depth = 4; // pfm is always float
255
256 loadTexture_PFM_readFile(file, scaleFactor);
257
258 if (!texelData.get())
259 std::cerr << "#osp:sg: INVALID FORMAT PFM " << params.components
260 << std::endl;
261
262 } catch (const std::runtime_error &e) {
263 std::cerr << "#osp:sg: INVALID PFM" << std::endl;
264 std::cerr << e.what() << std::endl;
265 }
266
267 if (file)
268 fclose(file);
269
270 if (!texelData.get()) {
271 std::cerr << "#osp:sg: PFM failed to load texture '" << fileName << "'"
272 << std::endl;
273 }
274 }
275
276 //
277 // STBi
278 //
loadTexture_STBi(const std::string & fileName)279 void Texture2D::loadTexture_STBi(const std::string &fileName)
280 {
281 stbi_set_flip_vertically_on_load(params.flip);
282
283 const bool isHDR = stbi_is_hdr(fileName.c_str());
284 const bool is16b = stbi_is_16_bit(fileName.c_str());
285
286 void *texels{nullptr};
287 int width, height;
288 if (isHDR)
289 texels = (void *)stbi_loadf(
290 fileName.c_str(), &width, &height, ¶ms.components, 0);
291 else if (is16b)
292 texels = (void *)stbi_load_16(
293 fileName.c_str(), &width, &height, ¶ms.components, 0);
294 else
295 texels = (void *)stbi_load(
296 fileName.c_str(), &width, &height, ¶ms.components, 0);
297
298 // Set flip on load back to default, STBi maintains a static global.
299 stbi_set_flip_vertically_on_load(0);
300
301 params.size = vec2ul(width, height);
302 params.depth = isHDR ? 4 : is16b ? 2 : 1;
303
304 if (texels) {
305 // XXX stbi uses malloc/free override these with our alignedMalloc/Free
306 // (and implement a realloc?) to prevent this memcpy?
307 size_t size = params.size.product() * params.components * params.depth;
308 std::shared_ptr<void> data(new uint8_t[size]);
309 std::memcpy(data.get(), texels, size);
310 texelData = data;
311 stbi_image_free(texels);
312 }
313
314 if (!texelData.get()) {
315 std::cerr << "#osp:sg: STB_image failed to load texture '" + fileName + "'"
316 << std::endl;
317 std::cerr << "#osp:sg: Rebuilding OSPRay Studio with OpenImageIO "
318 << "support may fix this error." << std::endl;
319 }
320 }
321 #endif
322
323 // Texture2D UDIM ///////////////////////////////////////////////////////////
324
325 // Check texture filename for udim pattern. Then check that each tile file
326 // exists. Image size/format will be checked on load. This is a quick check.
checkForUDIM(FileName filename)327 bool Texture2D::checkForUDIM(FileName filename)
328 {
329 std::string fullName = filename.str();
330
331 // Quick return if texture is already UDIM
332 if (hasUDIM())
333 return true;
334
335 // Make sure base file even exists
336 std::ifstream f(fullName.c_str());
337 if (!f.good())
338 return false;
339
340 // See if base tile "1001" is in the filename. If not, it's not a UDIM.
341 auto found = fullName.rfind("1001");
342 if (found == std::string::npos)
343 return false;
344
345 // Strip off the "1001" and continue searching for other tiles
346 // by checking existing files of the correct pattern.
347 // This will work for most any consistent *1001* naming scheme.
348 // pattern: lFileName<tileNum>rFileName
349 std::string lFileName = fullName.substr(0, found);
350 std::string rFileName = fullName.substr(found + 4);
351
352 int vmax = 0;
353 int umax = 0;
354 for (int v = 1; v <= 10; v++)
355 for (int u = 1; u <= 10; u++) {
356 std::string tileNum = std::to_string(1000 + (v - 1) * 10 + u);
357 std::string checkName = lFileName + tileNum + rFileName;
358 std::ifstream f(checkName.c_str());
359 if (f.good()) {
360 udimTile tile(checkName, vec2i(u - 1, v - 1));
361 udim_params.tiles.push_back(tile);
362 vmax = std::max(vmax, v);
363 umax = std::max(umax, u);
364 }
365 }
366
367 if (umax > 1) {
368 udim_params.dims.y = vmax;
369 udim_params.dims.x = vmax > 1 ? 10 : umax;
370 }
371
372 return umax > 1;
373 }
374
loadUDIM_tiles(const FileName & fileName)375 void Texture2D::loadUDIM_tiles(const FileName &fileName)
376 {
377 if (udim_params.tiles.size() < 2) {
378 std::cerr << "#osp:sg: loadUDIM_tiles: not a udim atlas" << std::endl;
379 return;
380 }
381
382 // Create two temporary working nodes
383 // work contains each loaded tile and builds a texture atlas into main
384 auto atlas = &createChildAs<Texture2D>("udim_main", "texture_2d");
385 auto work = &createChildAs<Texture2D>("udim_work", "texture_2d");
386
387 // Use the same params as parent texture
388 // but, mark as "loading" textures to skip re-checking udim tiles
389 work->params = params;
390 work->udim_params.loading = true;
391
392 // Load the first tile to establish tile parameters
393 auto tile = udim_params.tiles.front();
394 work->load(tile.first);
395 udim_params.tiles.pop_front();
396
397 auto tileSize = work->params.size;
398 auto tileDepth = work->params.depth;
399 auto tileComponents = work->params.components;
400 auto texelSize = tileDepth * tileComponents;
401 auto tileStride = tileSize.x * texelSize;
402
403 // Allocate space large enough to hold all tiles (all tiles guaranteed to be
404 // of equal size and format)
405 atlas->params = work->params;
406 atlas->udim_params = work->udim_params;
407 atlas->params.size *= udim_params.dims;
408 std::shared_ptr<void> data(
409 new uint8_t[atlas->params.size.product() * texelSize]);
410 atlas->texelData = data;
411 auto atlasStride = atlas->params.size.x * texelSize;
412
413 // Lambda to copy work tile into atlas
414 auto CopyTile = [&](vec2i origin) {
415 uint8_t *dest = (uint8_t *)data.get() + origin.y * tileSize.y * atlasStride
416 + origin.x * tileStride;
417 uint8_t *src = (uint8_t *)work->texelData.get();
418 for (int y = 0; y < tileSize.y; y++)
419 std::memcpy(dest + y * atlasStride, src + y * tileStride, tileStride);
420 };
421
422 CopyTile(tile.second);
423
424 // Load the remaining tiles into the atlas
425 for (const auto &tile : udim_params.tiles) {
426 work->load(tile.first);
427 // XXX TODO, allow different size/format tiles?
428 // This would require pre-loading all tiles and setting atlas to multiple
429 // of the largest size, then scaling all tiles into the atlas.
430 if (work->params.size != tileSize || work->params.depth != tileDepth
431 || work->params.components != tileComponents) {
432 std::cerr
433 << "#osp:sg: udim tile size or format doesn't match, skipping: "
434 << tile.first << std::endl;
435 continue;
436 }
437
438 CopyTile(tile.second);
439
440 // Don't keep tiles in the texture cache
441 textureCache.erase(tile.first);
442 }
443
444 // Copy atlas back to parent
445 params = atlas->params;
446 texelData = atlas->texelData;
447 for (auto &c : atlas->children())
448 add(c.second);
449
450 // Remove the temporary working nodes
451 remove("udim_work");
452 remove("udim_main");
453 }
454
455 // Texture2D public methods /////////////////////////////////////////////////
456
load(const FileName & _fileName,const bool _preferLinear,const bool _nearestFilter,const int _colorChannel)457 void Texture2D::load(const FileName &_fileName,
458 const bool _preferLinear,
459 const bool _nearestFilter,
460 const int _colorChannel)
461 {
462 fileName = _fileName;
463
464 // Check the cache before creating a new texture
465 if (textureCache.find(fileName) != textureCache.end()) {
466 std::shared_ptr<Texture2D> cache = textureCache[fileName].lock();
467 if (cache) {
468 params = cache->params;
469 udim_params = cache->udim_params;
470 // Copy shared_ptr ownership
471 texelData = cache->texelData;
472 }
473 } else {
474 // Check if fileName indicates a UDIM atlas and load tiles
475 if (!udim_params.loading && checkForUDIM(fileName))
476 loadUDIM_tiles(fileName);
477 else {
478 #ifdef USE_OPENIMAGEIO
479 loadTexture_OIIO(fileName);
480 #else
481 if (_fileName.ext() == "pfm")
482 loadTexture_PFM(fileName);
483 else
484 loadTexture_STBi(fileName);
485 #endif
486 }
487
488 // Add this texture to the cache
489 if (texelData.get())
490 textureCache[fileName] = this->nodeAs<Texture2D>();
491 }
492
493 if (texelData.get()) {
494 params.preferLinear = _preferLinear;
495 params.nearestFilter = _nearestFilter;
496 params.colorChannel = _colorChannel;
497
498 createDataNode();
499
500 // If the load was successful, populate children
501 if (hasChild("data")) {
502 child("data").setSGNoUI();
503
504 // If not using all channels, set used components to 1 for texture format
505 auto ospTexFormat =
506 osprayTextureFormat(params.colorChannel < 4 ? 1 : params.components);
507 auto texFilter = params.nearestFilter ? OSP_TEXTURE_FILTER_NEAREST
508 : OSP_TEXTURE_FILTER_BILINEAR;
509
510 createChild("format", "int", (int)ospTexFormat);
511 createChild("filter", "int", (int)texFilter);
512
513 createChild("filename", "string", fileName);
514 child("filename").setSGOnly();
515
516 child("format").setMinMax((int)OSP_TEXTURE_RGBA8, (int)OSP_TEXTURE_R16);
517 child("filter").setMinMax(
518 (int)OSP_TEXTURE_FILTER_BILINEAR, (int)OSP_TEXTURE_FILTER_NEAREST);
519 } else
520 std::cerr << "Failed texture " << fileName << std::endl;
521 }
522 }
523
load(void * memory,const bool _preferLinear,const bool _nearestFilter,const int _colorChannel)524 void Texture2D::load(void *memory,
525 const bool _preferLinear,
526 const bool _nearestFilter,
527 const int _colorChannel)
528 {
529 std::stringstream ss;
530 ss << "memory: " << std::hex << memory;
531 fileName = ss.str();
532
533 // Check the cache before creating a new texture
534 if (textureCache.find(fileName) != textureCache.end()) {
535 std::shared_ptr<Texture2D> cache = textureCache[fileName].lock();
536 if (cache) {
537 params = cache->params;
538 udim_params = cache->udim_params;
539 // Copy shared_ptr ownership
540 texelData = cache->texelData;
541 }
542 } else {
543 if (memory) {
544 size_t size = params.size.product() * params.components * params.depth;
545 std::shared_ptr<void> data(new uint8_t[size]);
546 std::memcpy(data.get(), memory, size);
547 // Move shared_ptr ownership
548 texelData = data;
549
550 // Add this texture to the cache
551 textureCache[fileName] = this->nodeAs<Texture2D>();
552 }
553 }
554
555 if (texelData.get()) {
556 params.preferLinear = _preferLinear;
557 params.nearestFilter = _nearestFilter;
558 params.colorChannel = _colorChannel;
559
560 createDataNode();
561
562 // If the load was successful, populate children
563 if (hasChild("data")) {
564 child("data").setSGNoUI();
565
566 // If not using all channels, set used components to 1 for texture format
567 auto ospTexFormat =
568 osprayTextureFormat(params.colorChannel < 4 ? 1 : params.components);
569 auto texFilter = params.nearestFilter ? OSP_TEXTURE_FILTER_NEAREST
570 : OSP_TEXTURE_FILTER_BILINEAR;
571
572 createChild("format", "int", (int)ospTexFormat);
573 createChild("filter", "int", (int)texFilter);
574
575 createChild("filename", "string", fileName);
576 child("filename").setSGOnly();
577
578 child("format").setMinMax((int)OSP_TEXTURE_RGBA8, (int)OSP_TEXTURE_R16);
579 child("filter").setMinMax(
580 (int)OSP_TEXTURE_FILTER_BILINEAR, (int)OSP_TEXTURE_FILTER_NEAREST);
581 } else
582 std::cerr << "Failed texture " << fileName << std::endl;
583 }
584 }
585
586 // Texture2D definitions ////////////////////////////////////////////////////
587
Texture2D()588 Texture2D::Texture2D() : Texture("texture2d") {}
~Texture2D()589 Texture2D::~Texture2D()
590 {
591 textureCache.erase(fileName);
592 }
593
preCommit()594 void Texture2D::preCommit()
595 {
596 // make sure to call base-class precommit
597 Texture::preCommit();
598 }
599
postCommit()600 void Texture2D::postCommit()
601 {
602 Texture::postCommit();
603 }
604
605 OSP_REGISTER_SG_NODE_NAME(Texture2D, texture_2d);
606
607 std::map<std::string, std::weak_ptr<Texture2D>> Texture2D::textureCache;
608
609 } // namespace sg
610 } // namespace ospray
611