Radium Engine  1.5.0
EnvironmentTexture.cpp
1 #include <algorithm>
2 #include <cmath>
3 #include <filesystem>
4 #include <iostream>
5 
6 #include <cstring>
7 
8 #include <Engine/Data/EnvironmentTexture.hpp>
9 
10 #include <stb/stb_image.h>
11 #include <stb/stb_image_write.h>
12 
13 #include <Core/Asset/Camera.hpp>
14 #include <Core/Geometry/MeshPrimitives.hpp>
15 #include <Core/Math/Math.hpp>
16 #include <Core/Resources/Resources.hpp>
17 
18 #include <Engine/Data/Mesh.hpp>
19 #include <Engine/Data/ShaderProgram.hpp>
20 #include <Engine/Data/ShaderProgramManager.hpp>
21 #include <Engine/Data/Texture.hpp>
22 #include <Engine/Data/ViewingParameters.hpp>
23 #include <Engine/RadiumEngine.hpp>
24 #include <Engine/Scene/CameraManager.hpp>
25 
26 #include <tinyEXR/tinyexr.h>
27 
28 namespace Ra {
29 namespace Engine {
30 namespace Data {
31 using namespace gl;
32 
33 // Flip horizontally an image of w x h pixels with c commponents
34 template <typename T>
35 void flip_horizontally( T* img, size_t w, size_t h, size_t c ) {
36 #pragma omp parallel for firstprivate( img )
37  for ( int r = 0; r < int( h ); ++r ) {
38  auto limg = img + r * ( w * c );
39  for ( int l = 0; l < int( w ) / 2; ++l ) {
40  T* from = limg + ( l * c );
41  T* to = limg + ( w - ( l + 1 ) ) * c;
42  for ( int e = 0; e < int( c ); ++e ) {
43  std::swap( *( from + e ), *( to + e ) );
44  }
45  }
46  }
47 }
48 
49 // -------------------------------------------------------------------
50 class PfmReader
51 {
52  public:
53  typedef struct {
54  float r, g, b;
55  } HDRPIXEL;
56 
57  typedef struct {
58  int width;
59  int height;
60  HDRPIXEL* pixels;
61  } HDRIMAGE;
62  /*
63  Read a possibly byte swapped floating point number
64  Assume IEEE format
65  */
66  static HDRIMAGE* open( std::string fileName ) {
67  FILE* fptr = fopen( fileName.c_str(), "rb" );
68  if ( nullptr == fptr ) {
69  std::cerr << "could not open " << fileName << std::endl;
70  return nullptr;
71  }
72  int width, height, color, swap;
73  if ( !readHeader( fptr, width, height, color, swap ) ) {
74  std::cerr << "bad header " << fileName << std::endl;
75  return nullptr;
76  }
77 
78  HDRPIXEL* pixels = new HDRPIXEL[width * height];
79  for ( int i = 0; i < width * height; ++i ) {
80  float* ptr = &( pixels[i].r );
81  if ( !readFloat( fptr, ptr, swap ) ) {
82  std::cerr << "could not read pixel " << i << std::endl;
83  }
84  if ( color == 3 ) {
85  if ( !readFloat( fptr, ptr + 1, swap ) ) {
86  std::cerr << "could not read pixel " << i << std::endl;
87  }
88  if ( !readFloat( fptr, ptr + 2, swap ) ) {
89  std::cerr << "could not read pixel " << i << std::endl;
90  }
91  }
92  }
93  HDRIMAGE* ret = new HDRIMAGE;
94  ret->width = width;
95  ret->height = height;
96  ret->pixels = pixels;
97  return ret;
98  }
99 
100  static bool readFloat( FILE* fptr, float* n, int swap ) {
101 
102  if ( fread( n, 4, 1, fptr ) != 1 ) return false;
103  if ( swap ) {
104  unsigned char* cptr = (unsigned char*)n;
105  std::swap( cptr[0], cptr[3] );
106  std::swap( cptr[1], cptr[2] );
107  }
108 
109  return true;
110  }
111 
112  static bool readHeader( FILE* fptr, int& width, int& height, int& color, int& swap ) {
113  char buf[32];
114  // read first line
115  if ( NULL == fgets( buf, 32, fptr ) ) return false;
116  // determine gray or color : PF -> color, Pf -> gray
117  if ( 0 == strncmp( "PF", buf, 2 ) )
118  color = 3;
119  else if ( 0 == strncmp( "Pf", buf, 2 ) )
120  color = 1;
121  else
122  return false;
123  // read second line
124  if ( NULL == fgets( buf, 32, fptr ) ) return false;
125  // determine image size
126  char* endptr = NULL;
127  width = strtol( buf, &endptr, 10 );
128  if ( endptr == buf ) return false;
129  height = strtol( endptr, (char**)NULL, 10 );
130  // read third line
131  if ( NULL == fgets( buf, 32, fptr ) ) return false;
132  if ( endptr == buf ) return false;
133  float aspect = strtof( buf, &endptr );
134  if ( aspect > 0 )
135  swap = 1;
136  else
137  swap = 0;
138  return true;
139  }
140 };
141 
142 // -------------------------------------------------------------------
143 using namespace Ra::Core;
144 
145 // -------------------------------------------------------------------
146 EnvironmentTexture::EnvironmentTexture( const std::string& mapName, bool isSkybox ) :
147  m_name( mapName ),
148  m_skyData { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr },
149  m_isSkyBox( isSkybox ) {
150  m_type = ( mapName.find( ';' ) != mapName.npos ) ? EnvironmentTexture::EnvMapType::ENVMAP_CUBE
151  : EnvironmentTexture::EnvMapType::ENVMAP_PFM;
152  if ( m_type == EnvironmentTexture::EnvMapType::ENVMAP_PFM ) {
153  auto ext = mapName.substr( mapName.size() - 3 );
154  if ( ext != "pfm" ) { m_type = EnvironmentTexture::EnvMapType::ENVMAP_LATLON; }
155  }
156  initializeTexture();
157 }
158 
159 void EnvironmentTexture::initializeTexture() {
160  switch ( m_type ) {
161  case EnvMapType::ENVMAP_PFM:
162  setupTexturesFromPfm();
163  break;
164  case EnvMapType::ENVMAP_CUBE:
165  setupTexturesFromCube();
166  break;
167  case EnvMapType::ENVMAP_LATLON:
168  setupTexturesFromSphericalEquiRectangular();
169  break;
170  default:
171  LOG( logERROR ) << "EnvironmentTexture::initializeTexture(): unkown EnvMapType";
172  }
173  computeSHMatrices();
174  // make the envmap cube texture
175  Ra::Engine::Data::TextureParameters params { m_name,
176  GL_TEXTURE_CUBE_MAP,
177  m_width,
178  m_height,
179  1,
180  GL_RGBA,
181  GL_RGBA,
182  GL_FLOAT,
183  GL_CLAMP_TO_EDGE,
184  GL_CLAMP_TO_EDGE,
185  GL_CLAMP_TO_EDGE,
186  GL_LINEAR_MIPMAP_LINEAR,
187  GL_LINEAR,
188  (void**)( m_skyData ) };
189  m_skyTexture = std::make_unique<Ra::Engine::Data::Texture>( params );
190 
191  if ( m_isSkyBox ) {
192  // make the skybox geometry
193  Aabb aabb( Vector3 { -1_ra, -1_ra, -1_ra }, Vector3 { 1_ra, 1_ra, 1_ra } );
194  Geometry::TriangleMesh skyMesh = Geometry::makeSharpBox( aabb );
195  m_displayMesh = std::make_unique<Ra::Engine::Data::Mesh>( "skyBox" );
196  m_displayMesh->loadGeometry( std::move( skyMesh ) );
197  }
198 }
199 
200 void EnvironmentTexture::setupTexturesFromPfm() {
201  PfmReader::HDRIMAGE* img = PfmReader::open( m_name.c_str() );
202  m_width = img->width / 3;
203  m_height = img->height / 4;
204 
205  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
206  m_skyData[imgIdx] = new float[m_width * m_height * 4];
207  }
208 
209 #pragma omp parallel for
210  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
211  int xOffset = 0;
212  int yOffset = 0;
213  switch ( imgIdx ) {
214  case 0: // X- side
215  xOffset = 1 * m_width;
216  yOffset = 0 * m_height;
217  break;
218  case 1: // X+ side
219  xOffset = 1 * m_width;
220  yOffset = 2 * m_height;
221  break;
222  case 2: // Y+ side
223  xOffset = 1 * m_width;
224  yOffset = 3 * m_height;
225  break;
226  case 3: // Y- side
227  xOffset = 1 * m_width;
228  yOffset = 1 * m_height;
229  break;
230  case 4: // Z+ side
231  xOffset = 0 * m_width;
232  yOffset = 2 * m_height;
233  break;
234  case 5: // Z- side
235  xOffset = 2 * m_width;
236  yOffset = 2 * m_height;
237  break;
238  default:
239  xOffset = 0;
240  yOffset = 0;
241  break;
242  }
243  for ( size_t i = 0; i < m_height; ++i ) {
244  for ( size_t j = 0; j < m_width; ++j ) {
245  if ( imgIdx == 1 || imgIdx == 4 || imgIdx == 5 ) {
246  m_skyData[imgIdx][4 * ( ( m_height - 1 - i ) * m_width + j ) + 0] =
247  img->pixels[( i + yOffset ) * img->width + j + xOffset].r;
248  m_skyData[imgIdx][4 * ( ( m_height - 1 - i ) * m_width + j ) + 1] =
249  img->pixels[( i + yOffset ) * img->width + j + xOffset].g;
250  m_skyData[imgIdx][4 * ( ( m_height - 1 - i ) * m_width + j ) + 2] =
251  img->pixels[( i + yOffset ) * img->width + j + xOffset].b;
252  m_skyData[imgIdx][4 * ( ( m_height - 1 - i ) * m_width + j ) + 3] = 1.f;
253  }
254  else {
255  if ( imgIdx == 2 ) {
256  m_skyData[imgIdx]
257  [4 * ( ( m_width - 1 - j ) * m_height + m_height - 1 - i ) + 0] =
258  img->pixels[( i + yOffset ) * img->width + j + xOffset].r;
259  m_skyData[imgIdx]
260  [4 * ( ( m_width - 1 - j ) * m_height + m_height - 1 - i ) + 1] =
261  img->pixels[( i + yOffset ) * img->width + j + xOffset].g;
262  m_skyData[imgIdx]
263  [4 * ( ( m_width - 1 - j ) * m_height + m_height - 1 - i ) + 2] =
264  img->pixels[( i + yOffset ) * img->width + j + xOffset].b;
265  m_skyData[imgIdx]
266  [4 * ( ( m_width - 1 - j ) * m_height + m_height - 1 - i ) + 3] =
267  1.f;
268  }
269  else {
270  if ( imgIdx == 0 ) {
271  m_skyData[imgIdx][4 * ( i * m_width + m_width - 1 - j ) + 0] =
272  img->pixels[( i + yOffset ) * img->width + j + xOffset].r;
273  m_skyData[imgIdx][4 * ( i * m_width + m_width - 1 - j ) + 1] =
274  img->pixels[( i + yOffset ) * img->width + j + xOffset].g;
275  m_skyData[imgIdx][4 * ( i * m_width + m_width - 1 - j ) + 2] =
276  img->pixels[( i + yOffset ) * img->width + j + xOffset].b;
277  m_skyData[imgIdx][4 * ( i * m_width + m_width - 1 - j ) + 3] = 1.f;
278  }
279  else {
280  // imgIdx == 3
281  m_skyData[imgIdx][4 * ( j * m_width + i ) + 0] =
282  img->pixels[( i + yOffset ) * img->width + j + xOffset].r;
283  m_skyData[imgIdx][4 * ( j * m_width + i ) + 1] =
284  img->pixels[( i + yOffset ) * img->width + j + xOffset].g;
285  m_skyData[imgIdx][4 * ( j * m_width + i ) + 2] =
286  img->pixels[( i + yOffset ) * img->width + j + xOffset].b;
287  m_skyData[imgIdx][4 * ( j * m_width + i ) + 3] = 1.f;
288  }
289  }
290  }
291  }
292  }
293  }
294 
295  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
296  flip_horizontally( m_skyData[imgIdx], m_width, m_height, 4 );
297  }
298 
299  delete[] img->pixels;
300  delete img;
301 }
302 
303 void EnvironmentTexture::setupTexturesFromCube() {
304  std::stringstream imgs( m_name );
305  std::string imgname;
306  while ( getline( imgs, imgname, ';' ) ) {
307  int imgIdx { -1 };
308  bool flipV { false };
309  // is it a +X face ?
310  if ( ( imgname.find( "posx" ) != imgname.npos ) ||
311  ( imgname.find( "-X-plux" ) != imgname.npos ) ) {
312  imgIdx = 0;
313  }
314  // is it a -X face ?
315  if ( ( imgname.find( "negx" ) != imgname.npos ) ||
316  ( imgname.find( "-X-minux" ) != imgname.npos ) ) {
317  imgIdx = 1;
318  }
319  // is it a +Y face ?
320  if ( ( imgname.find( "posy" ) != imgname.npos ) ||
321  ( imgname.find( "-Y-plux" ) != imgname.npos ) ) {
322  imgIdx = 2;
323  flipV = true;
324  }
325  // is it a -Y face ?
326  if ( ( imgname.find( "negy" ) != imgname.npos ) ||
327  ( imgname.find( "-Y-minux" ) != imgname.npos ) ) {
328  imgIdx = 3;
329  flipV = true;
330  }
331  // is it a +Z face ? --> goes to the -Z of cubemap, need to flip horizontally
332  if ( ( imgname.find( "posz" ) != imgname.npos ) ||
333  ( imgname.find( "-Z-plux" ) != imgname.npos ) ) {
334  imgIdx = 5;
335  }
336  // is it a -Z face ? --> goes to the +Z of cubemap, need to flip horizontally
337  if ( ( imgname.find( "negz" ) != imgname.npos ) ||
338  ( imgname.find( "-Z-minux" ) != imgname.npos ) ) {
339  imgIdx = 4;
340  }
341 
342  int n;
343  int w;
344  int h;
345  stbi_set_flip_vertically_on_load( flipV );
346  // Assume input images are in sRGB color space. stbi_loadf will convert to linear space.
347  auto loaded = stbi_loadf( imgname.c_str(), &w, &h, &n, 0 );
348  if ( !loaded ) {
349  LOG( logERROR ) << "EnvironmentTexture::setupTexturesFromCube : unable to load "
350  << imgname.c_str();
351  }
352  m_width = w;
353  m_height = h;
354  m_skyData[imgIdx] = new float[m_width * m_height * 4];
355 
356  for ( int l = 0; l < int( m_height ); ++l ) {
357  for ( int c = 0; c < int( m_width ); ++c ) {
358  auto is = ( l * m_width + c );
359  auto id = flipV ? is : ( l * m_width + ( m_width - c - 1 ) );
360  m_skyData[imgIdx][id * 4 + 0] = loaded[is * n + 0];
361  m_skyData[imgIdx][id * 4 + 1] = loaded[is * n + 1];
362  m_skyData[imgIdx][id * 4 + 2] = loaded[is * n + 2];
363  m_skyData[imgIdx][id * 4 + 3] = 0;
364  }
365  }
366  stbi_image_free( loaded );
367  }
368 }
369 
370 void EnvironmentTexture::setupTexturesFromSphericalEquiRectangular() {
371  auto ext = m_name.substr( m_name.size() - 3 );
372  float* latlonPix { nullptr };
373  int n, w, h;
374  if ( ext == "exr" ) {
375  const char* err { nullptr };
376  // EXR image are always in linear RGB color space
377  int ret = LoadEXR( &latlonPix, &w, &h, m_name.c_str(), &err );
378  n = 4;
379  if ( ret != TINYEXR_SUCCESS ) {
380  if ( err ) {
381  std::cerr << "EnvironmentTexture -- Error reading " << m_name << " : " << err
382  << std::endl;
383  FreeEXRErrorMessage( err ); // release memory of error message.
384  }
385  }
386  }
387  else {
388  stbi_set_flip_vertically_on_load( false );
389  // Assume input images are in sRGB color space. stbi_loadf will convert to linear space.
390  latlonPix = stbi_loadf( m_name.c_str(), &w, &h, &n, 4 );
391  }
392  int textureSize = 1;
393  while ( textureSize < h ) {
394  textureSize <<= 1;
395  }
396  textureSize >>= 1;
397  // Bases to use to convert sphericalequirectangular images to cube faces
398  // These bases allow to convert (u, v) cordinates of each faces to (x, y, z) in the frame of the
399  // equirectangular map. : (x, y, z) = u*A[0] + v*A[1] + A[2]
400  Vector3 bases[6][3] = { { { -1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 } },
401  { { 1, 0, 0 }, { 0, -1, 0 }, { 0, 0, -1 } },
402  { { 0, 0, 1 }, { 1, 0, 0 }, { 0, 1, 0 } },
403  { { 0, 0, -1 }, { 1, 0, 0 }, { 0, -1, 0 } },
404  { { 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, -1 } },
405  { { 0, -1, 0 }, { -1, 0, 0 }, { 0, 0, -1 } } };
406  auto sphericalPhi = []( const Vector3& d ) {
407  Scalar p = std::atan2( d.x(), d.y() );
408  return ( p < 0 ) ? ( p + 2 * M_PI ) : p;
409  };
410  auto sphericalTheta = []( const Vector3& d ) { return std::acos( d.z() ); };
411 
412  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
413  // Alllocate the images
414  m_skyData[imgIdx] = new float[textureSize * textureSize * 4];
415  }
416 
417  Scalar duv = 2_ra / textureSize;
418 
419 #pragma omp parallel for firstprivate( duv )
420  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
421  // Fill in pixels
422  for ( int i = 0; i < textureSize; i++ ) {
423  Scalar u = -1 + i * duv;
424  for ( int j = 0; j < textureSize; j++ ) {
425  Scalar v = -1 + j * duv;
426  Vector3 d = bases[imgIdx][0] + u * bases[imgIdx][1] + v * bases[imgIdx][2];
427  d = d.normalized();
428  Vector2 st { w * sphericalPhi( d ) / ( 2 * M_PI ), h * sphericalTheta( d ) / M_PI };
429  // TODO : use st to access and filter the original envmap
430  // for now, no filtering is done. (eq to GL_NEAREST)
431  int s = int( st.x() );
432  int t = int( st.y() );
433  int cu = int( ( u / 2 + 0.5 ) * textureSize );
434  int cv = int( ( v / 2 + 0.5 ) * textureSize );
435 
436  m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 0] =
437  latlonPix[4 * ( t * w + s ) + 0];
438  m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 1] =
439  latlonPix[4 * ( t * w + s ) + 1];
440  m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 2] =
441  latlonPix[4 * ( t * w + s ) + 2];
442  m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 3] = 1;
443  }
444  }
445  }
446 
447  for ( int imgIdx = 0; imgIdx < 6; ++imgIdx ) {
448  flip_horizontally( m_skyData[imgIdx], textureSize, textureSize, 4 );
449  }
450 
451  free( latlonPix );
452  m_width = m_height = textureSize;
453 }
454 
455 void EnvironmentTexture::computeSHMatrices() {
456  for ( int i = 0; i < 9; i++ ) {
457  for ( int j = 0; j < 3; j++ ) {
458  m_shcoefs[i][j] = 0.f;
459  }
460  }
463  constexpr Scalar dtheta = 0.005_ra;
464  constexpr Scalar dphi = 0.005_ra;
465  for ( Scalar theta = 0_ra; theta < M_PI; theta += dtheta ) {
466  for ( Scalar phi = 0_ra; phi < 2_ra * M_PI; phi += dphi ) {
467  Scalar x = std::sin( theta ) * std::cos( phi );
468  Scalar y = std::sin( theta ) * std::sin( phi );
469  Scalar z = std::cos( theta );
470  float* thePixel = getPixel( x, y, z );
471  updateCoeffs( thePixel, -x, y, z, std::sin( theta ) * dtheta * dphi );
472  }
473  }
474  tomatrix();
475 }
476 
477 void EnvironmentTexture::updateCoeffs( float* hdr, float x, float y, float z, float domega ) {
478  /******************************************************************
479  Notes: Of course, there are better numerical methods to do
480  integration, but this naive approach is sufficient for our
481  purpose.
482  *********************************************************************/
483  for ( int col = 0; col < 3; col++ ) {
484  float c; /* A different constant for each coefficient */
485 
486  /* L_{00}. Note that Y_{00} = 0.282095 */
487  c = 0.282095f;
488  m_shcoefs[0][col] += hdr[col] * c * domega;
489 
490  /* L_{1m}. -1 <= m <= 1. The linear terms */
491  c = 0.488603f;
492  m_shcoefs[1][col] += hdr[col] * ( c * y ) * domega; /* Y_{1-1} = 0.488603 y */
493  m_shcoefs[2][col] += hdr[col] * ( c * z ) * domega; /* Y_{10} = 0.488603 z */
494  m_shcoefs[3][col] += hdr[col] * ( c * x ) * domega; /* Y_{11} = 0.488603 x */
495 
496  /* The Quadratic terms, L_{2m} -2 <= m <= 2 */
497 
498  /* First, L_{2-2}, L_{2-1}, L_{21} corresponding to xy,yz,xz */
499  c = 1.092548f;
500  m_shcoefs[4][col] += hdr[col] * ( c * x * y ) * domega; /* Y_{2-2} = 1.092548 xy */
501  m_shcoefs[5][col] += hdr[col] * ( c * y * z ) * domega; /* Y_{2-1} = 1.092548 yz */
502  m_shcoefs[7][col] += hdr[col] * ( c * x * z ) * domega; /* Y_{21} = 1.092548 xz */
503 
504  /* L_{20}. Note that Y_{20} = 0.315392 (3z^2 - 1) */
505  c = 0.315392f;
506  m_shcoefs[6][col] += hdr[col] * ( c * ( 3 * z * z - 1 ) ) * domega;
507 
508  /* L_{22}. Note that Y_{22} = 0.546274 (x^2 - y^2) */
509  c = 0.546274f;
510  m_shcoefs[8][col] += hdr[col] * ( c * ( x * x - y * y ) ) * domega;
511  }
512 }
513 
514 void EnvironmentTexture::tomatrix( void ) {
515  /* Form the quadratic form matrix (see equations 11 and 12 in paper) */
516  int col;
517  constexpr float c1 = 0.429043f, c2 = 0.511664f, c3 = 0.743125f, c4 = 0.886227f, c5 = 0.247708f;
518 
519  for ( col = 0; col < 3; col++ ) { /* Equation 12 */
520  m_shMatrices[col] = ( Ra::Core::Matrix4() << c1 * m_shcoefs[8][col],
521  c1 * m_shcoefs[4][col],
522  c1 * m_shcoefs[7][col],
523  c2 * m_shcoefs[3][col],
524 
525  c1 * m_shcoefs[4][col],
526  -c1 * m_shcoefs[8][col],
527  c1 * m_shcoefs[5][col],
528  c2 * m_shcoefs[1][col],
529 
530  c1 * m_shcoefs[7][col],
531  c1 * m_shcoefs[5][col],
532  c3 * m_shcoefs[6][col],
533  c2 * m_shcoefs[2][col],
534 
535  c2 * m_shcoefs[3][col],
536  c2 * m_shcoefs[1][col],
537  c2 * m_shcoefs[2][col],
538  c4 * m_shcoefs[0][col] - c5 * m_shcoefs[6][col] )
539  .finished();
540  }
541 }
542 
543 float* EnvironmentTexture::getPixel( float x, float y, float z ) {
544  auto ma = std::abs( x );
545  int axis = ( x > 0 );
546  auto tc = axis ? z : -z;
547  auto sc = y;
548  if ( std::abs( y ) > ma ) {
549  ma = std::abs( y );
550  axis = 2 + ( y < 0 );
551  tc = -x;
552  sc = ( axis == 2 ) ? -z : z;
553  }
554  if ( std::abs( z ) > ma ) {
555  ma = std::abs( z );
556  axis = 4 + ( z < 0 );
557  tc = ( axis == 4 ) ? -x : x;
558  sc = y;
559  }
560  auto s = 0.5_ra * ( 1_ra + sc / ma );
561  auto t = 0.5_ra * ( 1_ra + tc / ma );
562  int is = int( s * m_width );
563  int it = int( t * m_height );
564  return &( m_skyData[axis][4 * ( ( m_height - 1 - is ) * m_width + it )] );
565 }
566 
567 Ra::Engine::Data::Texture* EnvironmentTexture::getSHImage() {
568  if ( m_shtexture != nullptr ) { return m_shtexture.get(); }
569 
570  size_t ambientWidth = 1024;
571  auto thepixels = new unsigned char[4 * ambientWidth * ambientWidth];
572 #pragma omp parallel for
573  for ( int i = 0; i < int( ambientWidth ); i++ ) {
574  for ( int j = 0; j < int( ambientWidth ); j++ ) {
575 
576  /* We now find the cartesian components for the point (i,j) */
577  Scalar u, v, r;
578 
579  v = ( ambientWidth / 2.0_ra - j ) /
580  ( ambientWidth / 2.0_ra ); /* v ranges from -1 to 1 */
581  u = ( ambientWidth / 2.0_ra - i ) /
582  ( ambientWidth / 2.0_ra ); /* u ranges from -1 to 1 */
583  r = sqrt( u * u + v * v ); /* The "radius" */
584  if ( r > 1.0_ra ) {
585  thepixels[4 * ( j * ambientWidth + i ) + 0] = 0;
586  thepixels[4 * ( j * ambientWidth + i ) + 1] = 0;
587  thepixels[4 * ( j * ambientWidth + i ) + 2] = 0;
588  thepixels[4 * ( j * ambientWidth + i ) + 3] = 255;
589  continue; /* Consider only circle with r<1 */
590  }
591 
592  Scalar theta = M_PI * r; /* theta parameter of (i,j) */
593  Scalar phi = atan2( v, u ); /* phi parameter */
594 
595  Scalar x = std::sin( theta ) * std::cos( phi ); /* Cartesian components */
596  Scalar y = std::sin( theta ) * std::sin( phi );
597  Scalar z = std::cos( theta );
598 
599  // color = NtMN
600  Ra::Core::Vector4 normal( x, y, z, 1_ra );
601  const Ra::Core::Vector4 lcolor { normal.dot( m_shMatrices[0] * normal ),
602  normal.dot( m_shMatrices[1] * normal ),
603  normal.dot( m_shMatrices[2] * normal ),
604  1_ra };
605  Ra::Core::Utils::Color color {
606  Ra::Core::Utils::Color::linearRGBTosRGB( lcolor * 0.05_ra ) };
607 
608  auto clpfnct = []( Scalar value ) { return std::clamp( value, 0_ra, 1_ra ); };
609 
610  color.unaryExpr( clpfnct );
611  thepixels[4 * ( j * ambientWidth + i ) + 0] =
612  static_cast<unsigned char>( color[0] * 255 );
613  thepixels[4 * ( j * ambientWidth + i ) + 1] =
614  static_cast<unsigned char>( color[1] * 255 );
615  thepixels[4 * ( j * ambientWidth + i ) + 2] =
616  static_cast<unsigned char>( color[2] * 255 );
617  thepixels[4 * ( j * ambientWidth + i ) + 3] = 255;
618  }
619  }
620  Ra::Engine::Data::TextureParameters params { "shImage",
621  GL_TEXTURE_2D,
622  ambientWidth,
623  ambientWidth,
624  1,
625  GL_RGBA,
626  GL_RGBA,
627  GL_UNSIGNED_BYTE,
628  GL_CLAMP_TO_EDGE,
629  GL_CLAMP_TO_EDGE,
630  GL_CLAMP_TO_EDGE,
631  GL_LINEAR,
632  GL_LINEAR,
633  thepixels };
634  m_shtexture = std::make_unique<Ra::Engine::Data::Texture>( params );
635  return m_shtexture.get();
636 }
637 
638 void EnvironmentTexture::saveShProjection( const std::string& filename ) {
639  auto tex = getSHImage();
640  auto flnm = std::string( "../" ) + filename;
641  stbi_write_png( flnm.c_str(), tex->width(), tex->height(), 4, tex->texels(), 0 );
642 }
643 
644 const Ra::Core::Matrix4& EnvironmentTexture::getShMatrix( int channel ) {
645  return m_shMatrices[channel];
646 }
647 
649  if ( !m_glReady ) {
650  // load the skybox shader
651  auto shaderMngr = Ra::Engine::RadiumEngine::getInstance()->getShaderProgramManager();
652  const std::string vertexShaderSource { "#include \"TransformStructs.glsl\"\n"
653  "layout (location = 0) in vec3 in_position;\n"
654  "out vec3 incidentDirection;\n"
655  "uniform Transform transform;\n"
656  "void main(void)\n"
657  "{\n"
658  " mat4 mvp = transform.proj * transform.view;\n"
659  " gl_Position = mvp*vec4(in_position, 1.0);\n"
660  " incidentDirection = in_position;\n"
661  "}\n" };
662  const std::string fragmentShadersource {
663  "layout (location = 0) out vec4 outColor;\n"
664  "in vec3 incidentDirection;\n"
665  "uniform samplerCube skyTexture;\n"
666  "uniform float strength;\n"
667  "void main(void)\n"
668  "{\n"
669  " vec3 envColor = texture(skyTexture, normalize(incidentDirection)).rgb;\n"
670  " outColor =vec4(strength*envColor, 1);\n"
671  "}\n" };
672  Ra::Engine::Data::ShaderConfiguration config { "EnvironmentTexture::Builtin SkyBox" };
673  config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX,
674  vertexShaderSource );
675  config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT,
676  fragmentShadersource );
677  auto added = shaderMngr->addShaderProgram( config );
678  if ( added ) { m_skyShader = added.value(); }
679  else {
680  // Unable to load the shader ... deactivate skybox
681  m_isSkyBox = false;
682  }
683  m_displayMesh->updateGL();
684  m_skyTexture->initializeGL();
685  glEnable( GL_TEXTURE_CUBE_MAP_SEAMLESS );
686  m_glReady = true;
687  // saveShProjection( "SHImage.png" );
688  }
689 }
690 
692  if ( m_isSkyBox ) {
693  // put this in a initializeGL method ?
694  if ( !m_glReady ) { updateGL(); }
695  auto skyparams = viewParams;
696  Ra::Core::Matrix3 t = Ra::Core::Transform { skyparams.viewMatrix }.linear();
697  skyparams.viewMatrix.setIdentity();
698  skyparams.viewMatrix.topLeftCorner<3, 3>() = t;
699  auto cameraManager = static_cast<Ra::Engine::Scene::CameraManager*>(
700  Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultCameraManager" ) );
701  auto cam = cameraManager->getActiveCamera();
702  Scalar fov =
703  std::clamp( cam->getZoomFactor() * cam->getFOV(), 0.001_ra, Math::Pi - 0.1_ra );
704  skyparams.projMatrix =
705  Ra::Core::Asset::Camera::perspective( cam->getAspect(), fov, 0.1_ra, 3._ra );
706  m_skyShader->bind();
707  m_skyShader->setUniform( "transform.proj", skyparams.projMatrix );
708  m_skyShader->setUniform( "transform.view", skyparams.viewMatrix );
709  m_skyTexture->bind( 0 );
710  m_skyShader->setUniform( "skytexture", 0 );
711  m_skyShader->setUniform( "strength", m_environmentStrength );
712  GLboolean depthEnabled;
713  glGetBooleanv( GL_DEPTH_WRITEMASK, &depthEnabled );
714  glDepthMask( GL_FALSE );
715  m_displayMesh->render( m_skyShader );
716  glDepthMask( depthEnabled );
717  }
718 }
719 
721  m_environmentStrength = s;
722 }
723 
725  return m_environmentStrength;
726 }
727 
729  return m_skyTexture.get();
730 }
731 
732 } // namespace Data
733 } // namespace Engine
734 } // namespace Ra
static Core::Matrix4 perspective(Scalar a, Scalar y, Scalar n, Scalar f)
Definition: Camera.cpp:84
static ColorBase linearRGBTosRGB(const ColorBase &lrgb)
convert the color expressed in linear RGB color space to sRGB
Definition: Color.hpp:72
Ra::Engine::Data::Texture * getEnvironmentTexture()
void render(const Ra::Engine::Data::ViewingParameters &viewParams)
Render the envmap as a textured cube. This method does nothing if the envmap is not a skybox.
const Ra::Core::Matrix4 & getShMatrix(int channel)
Return the SH Matrix corresponding to the given color channel. The SH matrix is computed according to...
void saveShProjection(const std::string &filename)
Saves the spherical image representing the SH-encoded envmap.
EnvironmentTexture(const std::string &mapName, bool isSkybox=false)
Construct an envmap from a file. Supported image component of the envmap are the following.
void updateGL()
Update the OpenGL state of the envmap : texture, skybox and shaders if needed.
float getStrength() const
Get the multiplicative factor which defined the power of the light source.
void setStrength(float s)
Set the multiplicative factor which defined the power of the light source.
void addShaderSource(ShaderType type, const std::string &source)
void setUniform(const char *name, const T &value) const
Uniform setters.
Ra::Core::Asset::Camera * getActiveCamera()
Get the pointer on the active camera data.
Definition: Cage.cpp:3
the set of viewing parameters extracted from the camera and given to the renderer