WebGL Month. Day 23. Skybox in WebGL

lesnitsky

Andrei Lesnitsky

Posted on July 23, 2019

WebGL Month. Day 23. Skybox in WebGL

This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo


Hey šŸ‘‹

Welcome to WebGL month.

In previous tutorials we've rendered objects without any surroundings, but what if we want to add sky to our scene?

There's a special texture type which mught help us with it

We can treat our scene as a giant cube where camera is always in the center of this cube.
So all we need it render this cube and apply a texture, like below

Skybox

Vertex shader will have vertex positions and texCoord attribute, view and projection matrix uniforms. We don't need model matrix as our "world" cube is static

šŸ“„ src/shaders/skybox.v.glsl



attribute vec3 position;
varying vec3 vTexCoord;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;

void main() {

}



Enter fullscreen mode Exit fullscreen mode

If our cube vertices coordinates are in [-1..1] range, we can use this coordinates as texture coordinates directly

šŸ“„ src/shaders/skybox.v.glsl



  uniform mat4 viewMatrix;

  void main() {
- 
+     vTexCoord = position;
  }



Enter fullscreen mode Exit fullscreen mode

And to calculate position of transformed vertex we need to multiply vertex position, view matrix and projection matrix

šŸ“„ src/shaders/skybox.v.glsl




  void main() {
      vTexCoord = position;
+     gl_Position = projectionMatrix * viewMatrix * vec4(position, 1.0);
  }



Enter fullscreen mode Exit fullscreen mode

Fragment shader should have a vTexCoord varying to receive tex coords from vertex shader

šŸ“„ src/shaders/skybox.f.glsl



precision mediump float;

varying vec3 vTexCoord;

void main() {

}



Enter fullscreen mode Exit fullscreen mode

and a special type of texture ā€“ sampler cube

šŸ“„ src/shaders/skybox.f.glsl



  precision mediump float;

  varying vec3 vTexCoord;
+ uniform samplerCube skybox;

  void main() {
- 
  }



Enter fullscreen mode Exit fullscreen mode

and all we need to calculate fragment color is to read color from cubemap texture

šŸ“„ src/shaders/skybox.f.glsl



  uniform samplerCube skybox;

  void main() {
+     gl_FragColor = textureCube(skybox, vTexCoord);
  }



Enter fullscreen mode Exit fullscreen mode

As usual we need to get a canvas reference, webgl context, and make canvas fullscreen

šŸ“„ src/skybox.js



const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

const width = document.body.offsetWidth;
const height = document.body.offsetHeight;

canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;

canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;



Enter fullscreen mode Exit fullscreen mode

Setup webgl program

šŸ“„ src/skybox.js



+ import vShaderSource from './shaders/skybox.v.glsl';
+ import fShaderSource from './shaders/skybox.f.glsl';
+ 
+ import { compileShader, setupShaderInput } from './gl-helpers';
+ 
  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');


  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
+ 
+ const vShader = gl.createShader(gl.VERTEX_SHADER);
+ const fShader = gl.createShader(gl.FRAGMENT_SHADER);
+ 
+ compileShader(gl, vShader, vShaderSource);
+ compileShader(gl, fShader, fShaderSource);
+ 
+ const program = gl.createProgram();
+ 
+ gl.attachShader(program, vShader);
+ gl.attachShader(program, fShader);
+ 
+ gl.linkProgram(program);
+ gl.useProgram(program);
+ 
+ const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);



Enter fullscreen mode Exit fullscreen mode

Create cube object and setup buffer for vertex positions

šŸ“„ src/skybox.js



  import fShaderSource from './shaders/skybox.f.glsl';

  import { compileShader, setupShaderInput } from './gl-helpers';
+ import { Object3D } from './Object3D';
+ import { GLBuffer } from './GLBuffer';
+ 
+ import cubeObj from '../assets/objects/cube.obj';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  gl.useProgram(program);

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);
+ 
+ const cube = new Object3D(cubeObj, [0, 0, 0], [0, 0, 0]);
+ const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.vertices, gl.STATIC_DRAW);



Enter fullscreen mode Exit fullscreen mode

Setup position attribute

šŸ“„ src/skybox.js




  const cube = new Object3D(cubeObj, [0, 0, 0], [0, 0, 0]);
  const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.vertices, gl.STATIC_DRAW);
+ 
+ vertexBuffer.bind(gl);
+ gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);



Enter fullscreen mode Exit fullscreen mode

Setup view, projection matrices, pass values to uniforms and set viewport

šŸ“„ src/skybox.js



  import { GLBuffer } from './GLBuffer';

  import cubeObj from '../assets/objects/cube.obj';
+ import { mat4 } from 'gl-matrix';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');

  vertexBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);
+ 
+ const viewMatrix = mat4.create();
+ const projectionMatrix = mat4.create();
+ 
+ mat4.lookAt(viewMatrix, [0, 0, 0], [0, 0, -1], [0, 1, 0]);
+ 
+ mat4.perspective(projectionMatrix, (Math.PI / 360) * 90, canvas.width / canvas.height, 0.01, 100);
+ 
+ gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);
+ gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);
+ 
+ gl.viewport(0, 0, canvas.width, canvas.height);



Enter fullscreen mode Exit fullscreen mode

And define a function which will render our scene

šŸ“„ src/skybox.js



  gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);

  gl.viewport(0, 0, canvas.width, canvas.height);
+ 
+ function frame() {
+     gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
+ 
+     requestAnimationFrame(frame);
+ }



Enter fullscreen mode Exit fullscreen mode

Now the fun part. Texture for each side of the cube should be stored in separate file, so we need to laod all images. Check out this site for other textures

šŸ“„ src/skybox.js



  import vShaderSource from './shaders/skybox.v.glsl';
  import fShaderSource from './shaders/skybox.f.glsl';

- import { compileShader, setupShaderInput } from './gl-helpers';
+ import { compileShader, setupShaderInput, loadImage } from './gl-helpers';
  import { Object3D } from './Object3D';
  import { GLBuffer } from './GLBuffer';

  import cubeObj from '../assets/objects/cube.obj';
  import { mat4 } from 'gl-matrix';

+ import rightTexture from '../assets/images/skybox/right.JPG';
+ import leftTexture from '../assets/images/skybox/left.JPG';
+ import upTexture from '../assets/images/skybox/up.JPG';
+ import downTexture from '../assets/images/skybox/down.JPG';
+ import backTexture from '../assets/images/skybox/back.JPG';
+ import frontTexture from '../assets/images/skybox/front.JPG';
+ 
  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');


      requestAnimationFrame(frame);
  }
+ 
+ Promise.all([
+     loadImage(rightTexture),
+     loadImage(leftTexture),
+     loadImage(upTexture),
+     loadImage(downTexture),
+     loadImage(backTexture),
+     loadImage(frontTexture),
+ ]).then((images) => {
+     frame();
+ });



Enter fullscreen mode Exit fullscreen mode

Now we need to create a webgl texture

šŸ“„ src/skybox.js



      loadImage(backTexture),
      loadImage(frontTexture),
  ]).then((images) => {
+     const texture = gl.createTexture();
+ 
      frame();
  });



Enter fullscreen mode Exit fullscreen mode

And pass a special texture type to bind method ā€“ gl.TEXTURE_CUBE_MAP

šŸ“„ src/skybox.js



      loadImage(frontTexture),
  ]).then((images) => {
      const texture = gl.createTexture();
+     gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);

      frame();
  });



Enter fullscreen mode Exit fullscreen mode

Then we need to setup texture

šŸ“„ src/skybox.js



      const texture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);

+     gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+     gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+     gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+     gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ 
      frame();
  });



Enter fullscreen mode Exit fullscreen mode

and upload each image to gpu

Targets are:

  • gl.TEXTURE_CUBE_MAP_POSITIVE_X ā€“ right
  • gl.TEXTURE_CUBE_MAP_NEGATIVE_X ā€“ left
  • gl.TEXTURE_CUBE_MAP_POSITIVE_Y ā€“ top
  • gl.TEXTURE_CUBE_MAP_NEGATIVE_Y ā€“ bottom
  • gl.TEXTURE_CUBE_MAP_POSITIVE_Z ā€“ front
  • gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ā€“ back

Since all these values are integers, we can iterate over all images and add image index to TEXTURE_CUBE_MAP_POSITIVE_X target

šŸ“„ src/skybox.js



      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

+     images.forEach((image, index) => {
+         gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + index, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+     });
+ 
      frame();
  });



Enter fullscreen mode Exit fullscreen mode

and finally let's reuse the code from previous tutorial to implement camera rotation animation

šŸ“„ src/skybox.js



  import { GLBuffer } from './GLBuffer';

  import cubeObj from '../assets/objects/cube.obj';
- import { mat4 } from 'gl-matrix';
+ import { mat4, vec3 } from 'gl-matrix';

  import rightTexture from '../assets/images/skybox/right.JPG';
  import leftTexture from '../assets/images/skybox/left.JPG';

  gl.viewport(0, 0, canvas.width, canvas.height);

+ const cameraPosition = [0, 0, 0];
+ const cameraFocusPoint = vec3.fromValues(0, 0, 1);
+ const cameraFocusPointMatrix = mat4.create();
+ 
+ mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);
+ 
  function frame() {
+     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [0, 0, -1]);
+     mat4.rotateY(cameraFocusPointMatrix, cameraFocusPointMatrix, Math.PI / 360);
+     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [0, 0, 1]);
+ 
+     mat4.getTranslation(cameraFocusPoint, cameraFocusPointMatrix);
+ 
+     mat4.lookAt(viewMatrix, cameraPosition, cameraFocusPoint, [0, 1, 0]);
+     gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);
+ 
      gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);

      requestAnimationFrame(frame);



Enter fullscreen mode Exit fullscreen mode

That's it, we now have a skybox which makes scene look more impressive šŸ˜Ž

Thanks for reading!

See you tomorrow šŸ‘‹


GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo

šŸ’– šŸ’Ŗ šŸ™… šŸš©
lesnitsky
Andrei Lesnitsky

Posted on July 23, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related