Search code examples
javascript3dwebglperspectivecamera

WebGL 3D perspective


I'm new to WebGL and I'm following the WebGL tutorial in https://webglfundamentals.org/. I draw a 3d cube in my canvas and It worked correct.

Then I tried 3D perspective from this. After that my cube stretched and it looks like this. (I used gl-matrix to do matrix calculations)

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

const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aColour;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    uniform vec2 uResolution;
    uniform float uFudgeFactor;
    varying vec4 vColour;

    void main() {
        vec4 position = uModelViewMatrix * uProjectionMatrix * aVertexPosition;
        vec4 zeroToOne = position / vec4(uResolution, uResolution.x, 1);
        vec4 zeroToTwo = zeroToOne * 2.0;
        vec4 clipSpace = zeroToTwo - 1.0;

        float zToDivideBy = 1.0 + clipSpace.z * uFudgeFactor;

        gl_Position = vec4(vec3(clipSpace.xy / zToDivideBy , clipSpace.z) * vec3(1, -1, 1), clipSpace.w);
        vColour = aColour;
    }
`
const fsSource = `
    precision mediump float;
    varying vec4 vColour;

    void main() {
      gl_FragColor = vColour;
    }
`

const program = initShaderProgram(vsSource, fsSource)
const programInfo = {
  program: program,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(program, "aVertexPosition"),
    color: gl.getAttribLocation(program, "aColour")
  },
  uniformLocations: {
    modelViewMatrix: gl.getUniformLocation(program, "uModelViewMatrix"),
    projectionMatrix: gl.getUniformLocation(program, "uProjectionMatrix"),
    resolution: gl.getUniformLocation(program, "uResolution"),
    fudgeFactor: gl.getUniformLocation(program, "uFudgeFactor")
  }
}

const model = mat4.create()
mat4.translate(model, model, [400, 200, 400])
const projectionMatrix = mat4.create()
mat4.perspective(projectionMatrix, Math.PI / 3, 2, 1, 1000)
let buffers = initBuffers();

requestAnimationFrame(drawScene);

function drawScene() {
  gl.viewport(0, 0, canvas.width, canvas.height)
  gl.clearColor(0, 0, 0, 1)
  gl.clearDepth(1)
  gl.enable(gl.DEPTH_TEST)
  gl.depthFunc(gl.LEQUAL)

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  gl.useProgram(programInfo.program)

  {
    const numComponents = 3;
    const type = gl.FLOAT
    const normalize = false
    const stride = 0
    const offset = 0
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position)
    gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition)
    gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset)
    draw3DBox(0, 0, 0, 100, 100, 100)
  }

  {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color)
    gl.enableVertexAttribArray(programInfo.attribLocations.color)
    gl.vertexAttribPointer(programInfo.attribLocations.color, 4, gl.FLOAT, false, 0, 0)
    setColours()
  }

  gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, model)
  gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix)
  gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height)
  gl.uniform1f(programInfo.uniformLocations.fudgeFactor, 0.4)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 24)

  requestAnimationFrame(drawScene)
}

function initShaderProgram(vsSource, fsSource) {
  const vertexShader = loadShader(gl.VERTEX_SHADER, vsSource)
  const fragmentShader = loadShader(gl.FRAGMENT_SHADER, fsSource)

  const shaderProgram = gl.createProgram()
  gl.attachShader(shaderProgram, vertexShader)
  gl.attachShader(shaderProgram, fragmentShader)
  gl.linkProgram(shaderProgram)

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    console.log("Unable to initialize the shader program >> ", gl.getProgramInfoLog(shaderProgram))
    return null
  }
  return shaderProgram
}

function loadShader(type, source) {
  const shader = gl.createShader(type)
  gl.shaderSource(shader, source)
  gl.compileShader(shader)

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.log("[ERROR] error occoured while compiling the shaders >> " + gl.getShaderInfoLog(shader))
    gl.deleteShader(shader)
    return null
  }
  return shader
}

function initBuffers() {
  const positionBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  const colorBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)

  return {
    color: colorBuffer,
    position: positionBuffer
  }
}

function draw3DBox(x, y, z, width, height, depth) {
  const positions = new Float32Array([
    // front
    x - width / 2, y - height / 2, z - depth / 2,
    x + width / 2, y - height / 2, z - depth / 2,
    x - width / 2, y + height / 2, z - depth / 2,
    x + width / 2, y + height / 2, z - depth / 2,
    // right
    x + width / 2, y + height / 2, z - depth / 2,
    x + width / 2, y - height / 2, z - depth / 2,
    x + width / 2, y + height / 2, z + depth / 2,
    x + width / 2, y - height / 2, z + depth / 2,
    // back
    x + width / 2, y - height / 2, z + depth / 2,
    x - width / 2, y - height / 2, z + depth / 2,
    x + width / 2, y + height / 2, z + depth / 2,
    x - width / 2, y + height / 2, z + depth / 2,
    // bottom
    x + width / 2, y + height / 2, z - depth / 2,
    x - width / 2, y + height / 2, z - depth / 2,
    x + width / 2, y + height / 2, z + depth / 2,
    x - width / 2, y + height / 2, z + depth / 2,
    // left
    x - width / 2, y + height / 2, z + depth / 2,
    x - width / 2, y + height / 2, z - depth / 2,
    x - width / 2, y - height / 2, z + depth / 2,
    x - width / 2, y - height / 2, z - depth / 2,
    // top
    x - width / 2, y - height / 2, z - depth / 2,
    x + width / 2, y - height / 2, z - depth / 2,
    x - width / 2, y - height / 2, z + depth / 2,
    x + width / 2, y - height / 2, z + depth / 2,
  ]);

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
}

function setColours() {
  const colours = [
    // front
    0, 0, 1, 1,
    0, 0, 1, 1,
    0, 0, 1, 1,
    0, 0, 1, 1,
    // right
    1, 0, 0, 1,
    1, 0, 0, 1,
    1, 0, 0, 1,
    1, 0, 0, 1,
    // back
    0, 0, 1, 1,
    0, 0, 1, 1,
    0, 0, 1, 1,
    0, 0, 1, 1,
    // bottom
    0, 1, 0, 1,
    0, 1, 0, 1,
    0, 1, 0, 1,
    0, 1, 0, 1,
    // left
    1, 0, 0, 1,
    1, 0, 0, 1,
    1, 0, 0, 1,
    1, 0, 0, 1,
    // top
    0, 1, 0, 1,
    0, 1, 0, 1,
    0, 1, 0, 1,
    0, 1, 0, 1,
  ]

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colours), gl.STATIC_DRAW)
}
<canvas width="800px" height="400px"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>

How can I fix this issue? Are there anything I'm doing wrong?

Thank you.


Solution

  • The shader code is incorrect for perspective.

    Here is what you have

        vec4 position = uModelViewMatrix * uProjectionMatrix * aVertexPosition;
        vec4 zeroToOne = position / vec4(uResolution, uResolution.x, 1);
        vec4 zeroToTwo = zeroToOne * 2.0;
        vec4 clipSpace = zeroToTwo - 1.0;
    
        float zToDivideBy = 1.0 + clipSpace.z * uFudgeFactor;
    
        gl_Position = vec4(vec3(clipSpace.xy / zToDivideBy , clipSpace.z) * vec3(1, -1, 1), clipSpace.w);
    

    this is what you need.

        gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    

    You said you started from this but none of that zeroToOne or u_resolution stuff is on that page. In fact resulting shader on that page is just

        gl_Position = u_matrix * position;
    

    I also moved the cube to be in the center in front of the camera. It was behind the camera. Camera's look down -Z by default

    const canvas = document.querySelector("canvas");
    const gl = canvas.getContext("webgl");
    
    const vsSource = `
        attribute vec4 aVertexPosition;
        attribute vec4 aColour;
    
        uniform mat4 uModelViewMatrix;
        uniform mat4 uProjectionMatrix;
        uniform vec2 uResolution;
        uniform float uFudgeFactor;
        varying vec4 vColour;
    
        void main() {
            gl_Position = uProjectionMatrix * uModelViewMatrix  * aVertexPosition;
            vColour = aColour;
        }
    `
    const fsSource = `
        precision mediump float;
        varying vec4 vColour;
    
        void main() {
          gl_FragColor = vColour;
        }
    `
    
    const program = initShaderProgram(vsSource, fsSource)
    const programInfo = {
      program: program,
      attribLocations: {
        vertexPosition: gl.getAttribLocation(program, "aVertexPosition"),
        color: gl.getAttribLocation(program, "aColour")
      },
      uniformLocations: {
        modelViewMatrix: gl.getUniformLocation(program, "uModelViewMatrix"),
        projectionMatrix: gl.getUniformLocation(program, "uProjectionMatrix"),
      }
    }
    
    const model = mat4.create()
    mat4.translate(model, model, [0, 0, -400])
    const projectionMatrix = mat4.create()
    mat4.perspective(projectionMatrix, Math.PI / 3, 2, 1, 1000)
    let buffers = initBuffers();
    
    requestAnimationFrame(drawScene);
    
    function drawScene() {
      gl.viewport(0, 0, canvas.width, canvas.height)
      gl.clearColor(0, 0, 0, 1)
      gl.clearDepth(1)
      gl.enable(gl.DEPTH_TEST)
      gl.depthFunc(gl.LEQUAL)
    
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    
      gl.useProgram(programInfo.program)
    
      {
        const numComponents = 3;
        const type = gl.FLOAT
        const normalize = false
        const stride = 0
        const offset = 0
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position)
        gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition)
        gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset)
        draw3DBox(0, 0, 0, 100, 100, 100)
      }
    
      {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color)
        gl.enableVertexAttribArray(programInfo.attribLocations.color)
        gl.vertexAttribPointer(programInfo.attribLocations.color, 4, gl.FLOAT, false, 0, 0)
        setColours()
      }
    
      gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, model)
      gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix)
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 24)
    
      requestAnimationFrame(drawScene)
    }
    
    function initShaderProgram(vsSource, fsSource) {
      const vertexShader = loadShader(gl.VERTEX_SHADER, vsSource)
      const fragmentShader = loadShader(gl.FRAGMENT_SHADER, fsSource)
    
      const shaderProgram = gl.createProgram()
      gl.attachShader(shaderProgram, vertexShader)
      gl.attachShader(shaderProgram, fragmentShader)
      gl.linkProgram(shaderProgram)
    
      if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        console.log("Unable to initialize the shader program >> ", gl.getProgramInfoLog(shaderProgram))
        return null
      }
      return shaderProgram
    }
    
    function loadShader(type, source) {
      const shader = gl.createShader(type)
      gl.shaderSource(shader, source)
      gl.compileShader(shader)
    
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.log("[ERROR] error occoured while compiling the shaders >> " + gl.getShaderInfoLog(shader))
        gl.deleteShader(shader)
        return null
      }
      return shader
    }
    
    function initBuffers() {
      const positionBuffer = gl.createBuffer()
      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
      const colorBuffer = gl.createBuffer()
      gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
    
      return {
        color: colorBuffer,
        position: positionBuffer
      }
    }
    
    function draw3DBox(x, y, z, width, height, depth) {
      const positions = new Float32Array([
        // front
        x - width / 2, y - height / 2, z - depth / 2,
        x + width / 2, y - height / 2, z - depth / 2,
        x - width / 2, y + height / 2, z - depth / 2,
        x + width / 2, y + height / 2, z - depth / 2,
        // right
        x + width / 2, y + height / 2, z - depth / 2,
        x + width / 2, y - height / 2, z - depth / 2,
        x + width / 2, y + height / 2, z + depth / 2,
        x + width / 2, y - height / 2, z + depth / 2,
        // back
        x + width / 2, y - height / 2, z + depth / 2,
        x - width / 2, y - height / 2, z + depth / 2,
        x + width / 2, y + height / 2, z + depth / 2,
        x - width / 2, y + height / 2, z + depth / 2,
        // bottom
        x + width / 2, y + height / 2, z - depth / 2,
        x - width / 2, y + height / 2, z - depth / 2,
        x + width / 2, y + height / 2, z + depth / 2,
        x - width / 2, y + height / 2, z + depth / 2,
        // left
        x - width / 2, y + height / 2, z + depth / 2,
        x - width / 2, y + height / 2, z - depth / 2,
        x - width / 2, y - height / 2, z + depth / 2,
        x - width / 2, y - height / 2, z - depth / 2,
        // top
        x - width / 2, y - height / 2, z - depth / 2,
        x + width / 2, y - height / 2, z - depth / 2,
        x - width / 2, y - height / 2, z + depth / 2,
        x + width / 2, y - height / 2, z + depth / 2,
      ]);
    
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
    }
    
    function setColours() {
      const colours = [
        // front
        0, 0, 1, 1,
        0, 0, 1, 1,
        0, 0, 1, 1,
        0, 0, 1, 1,
        // right
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1,
        // back
        0, 0, 1, 1,
        0, 0, 1, 1,
        0, 0, 1, 1,
        0, 0, 1, 1,
        // bottom
        0, 1, 0, 1,
        0, 1, 0, 1,
        0, 1, 0, 1,
        0, 1, 0, 1,
        // left
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1,
        // top
        0, 1, 0, 1,
        0, 1, 0, 1,
        0, 1, 0, 1,
        0, 1, 0, 1,
      ]
    
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colours), gl.STATIC_DRAW)
    }
    <canvas width="800px" height="400px"></canvas>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
    </script>