Published
Edited
Oct 18, 2020
1 star
Insert cell
Insert cell
canvas = document.createElement('canvas')
Insert cell
canvas.width = 800
Insert cell
canvas.height = 450
Insert cell
Insert cell
Insert cell
Insert cell
cubeVtxShaderSource = `
// Use high-precision floating-point values
precision highp float;

// Attributes are unique to each vertex
attribute vec3 position; // (X, Y, Z) coordinates in 3D space

// Uniforms can be updated once before the shader is invoked
// (i.e. before a draw call is issued, with drawArrays/drawElements/etc)
uniform mat4 projMat; // Projects from 3D (perspective) into 2D
uniform mat4 modelViewMat; // Applies camera and model transformations

void main(void) {
// Multiply the input vertex position by matrices, to project it into 2D screen space
// Write our vertex position to the built-in output variable gl_Position
gl_Position = projMat * modelViewMat * vec4(position, 1.0);
}
`
Insert cell
Insert cell
Insert cell
cubeFragShaderSource = `
// Use high-precision floating-point values
precision highp float;

void main(void) {
// Write our color to the built-in output variable gl_FragColor
gl_FragColor = vec4(0.9, 0.4, 0.6, 1.0);
}
`
Insert cell
Insert cell
Insert cell
function compileShader (gl, type, source) {
// Obtain a shader resource from WebGL
const shader = gl.createShader(type)
// Send our shader's source code to the WebGL implementation
gl.shaderSource(shader, source)
// Tell WebGL to compile our shader
gl.compileShader(shader)

// If our shader failed to compile...
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
// Get the shader's log from WebGL and write it to the browser console
console.error('Shader compilation failed: ' + gl.getShaderInfoLog(shader))
// Delete the shader resource (it is no longer useful)
gl.deleteShader(shader)
// We don't have a useful shader to return, so just return null (nothing)
return null
}

// Return the compiled shader
return shader
}
Insert cell
Insert cell
Insert cell
function initShaderProgram (gl, vtxSource, fragSource) {
// Compile our vertex shader from it's source
const vtxShader = compileShader(gl, gl.VERTEX_SHADER, vtxSource)
// Compile our fragment shader from it's source
const fragShader = compileShader(gl, gl.FRAGMENT_SHADER, fragSource)

// Obtain a shader program resource from WebGL
const program = gl.createProgram()
// Attach our vertex shader to the program
gl.attachShader(program, vtxShader)
// Attach our fragment shader to the program
gl.attachShader(program, fragShader)
// Link two attached shaders into one single program
// This is similar to how you might compile two C++ files separately, but
// pass them both to the linker to resolve dependencies and generate a
// single executable
gl.linkProgram(program)

// If our shader program failed to link (most likely because of an
// incompatibility between the two shaders in varyings/etc, or because we
// misused or didn't use appropriate built-in variables and functions)...
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
// Get the shader program's log from WebGL and write it to the browser console
console.error('Shader program linking failed: ' + gl.getProgramInfoLog(program))
// Delete shader and shader program resources
gl.deleteShader(vtxShader)
gl.deleteShader(fragShader)
gl.deleteProgram(program)
// We don't have a useful shader program to return, so just return null (nothing)
return null
}

return program
}
Insert cell
Insert cell
Insert cell
gl = canvas.getContext('webgl')
Insert cell
Insert cell
shaderProgram = initShaderProgram(gl, cubeVtxShaderSource, cubeFragShaderSource)
Insert cell
Insert cell
Insert cell
shaderProgramInfo = ({
attribs: {
position: gl.getAttribLocation(shaderProgram, 'position'),
},
uniforms: {
projMat: gl.getUniformLocation(shaderProgram, 'projMat'),
modelViewMat: gl.getUniformLocation(shaderProgram, 'modelViewMat'),
},
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createBuffer (gl, target, data, mode) {
const buffer = gl.createBuffer()
gl.bindBuffer(target, buffer)
gl.bufferData(target, data, mode)
return buffer
}
Insert cell
Insert cell
vtxPosBuf = createBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(cubeVtxPosArray), gl.STATIC_DRAW)
Insert cell
Insert cell
indexBuf = createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeIndexArray), gl.STATIC_DRAW)
Insert cell
Insert cell
Insert cell
glMatrix = require('https://cdn.jsdelivr.net/npm/gl-matrix@3.3.0/gl-matrix-min.min.js')
Insert cell
Insert cell
projMat = glMatrix.mat4.create()
Insert cell
(glMatrix.mat4.perspective(projMat, 45 * Math.PI / 180, canvas.width / canvas.height, 0.1, 1000.0))
Insert cell
Insert cell
viewMat = glMatrix.mat4.create()
Insert cell
(glMatrix.mat4.translate(viewMat, viewMat, [ 0.0, 0.0, -7.0 ]))
Insert cell
Insert cell
modelMat = glMatrix.mat4.create()
Insert cell
Insert cell
Insert cell
function animateModelMatrix () {
glMatrix.mat4.identity(modelMat)
glMatrix.mat4.rotateX(modelMat, modelMat, Math.sin(Date.now() / 750.0) * 2)
glMatrix.mat4.rotateY(modelMat, modelMat, Math.cos(Date.now() / 750.0) * 2)
}
Insert cell
Insert cell
modelViewMat = glMatrix.mat4.create()
Insert cell
Insert cell
Insert cell
(gl.viewport(0, 0, canvas.width, canvas.height))
Insert cell
(gl.clearColor(0.1, 0.1, 0.1, 1.0))
Insert cell
(gl.enable(gl.DEPTH_TEST))
Insert cell
Insert cell
function drawScene () {
// It's considered best practice to schedule another animation frame
// immediately when the current frame begins
window.requestAnimationFrame(drawScene)

// Call the function we declared earlier to animate the model matrix
animateModelMatrix()
// Multiply view matrix * model matrix and write result to modelViewMatrix
glMatrix.mat4.multiply(modelViewMat, viewMat, modelMat)

// Clear the screen to empty
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

// Use our shader program to draw this model
gl.useProgram(shaderProgram)

// Pass our matrix data to the shader program
// 4fv = 4D array of 4D floating-point vectors
gl.uniformMatrix4fv(shaderProgramInfo.uniforms.projMat, false, projMat)
gl.uniformMatrix4fv(shaderProgramInfo.uniforms.modelViewMat, false, modelViewMat)

// Bind our vertex position buffer
gl.bindBuffer(gl.ARRAY_BUFFER, vtxPosBuf)
// Specify layout of our vertex buffer.
// This flexibility offered by this function is most useful if you're using
// interleaved buffers, which we aren't.
//
// Arguments: attribute location, number of components per attribute, data type, normalized, stride
// Stride = "offset in bytes between the beginning of consecutive vertex attributes"
// 0 sets stride = number of components per attribute * component data type size
// Offset = "offset in bytes of the first component in the vertex attribute array"
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
gl.vertexAttribPointer(shaderProgramInfo.attribs.position, 3, gl.FLOAT, false, 0, 0)
// Enable our attribute location (basically: we bound useful data to it and
// the shader is going to access it)
gl.enableVertexAttribArray(shaderProgramInfo.attribs.position)

// Bind our index buffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuf)
// Issue a draw call!
//
// A "draw call" is a special API call that actually draws content on the screen
// So far, we've spent hundreds of lines of code setting up the pipeline, but
// nothing has been rendered yet.
// This call tells WebGL that we're done configuring things and want to use the
// resources we've bound to produce some graphical output.
//
// Arguments: mode (primitive type), number of elements, element data type, offset
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawElements
gl.drawElements(gl.TRIANGLES, cubeIndexArray.length, gl.UNSIGNED_SHORT, 0)
}
Insert cell
Insert cell
// We've disabled this function for now since we're about to create
// a new drawScene2 function that supports texturing and we'll want
// to use that instead.
//
// (window.requestAnimationFrame(drawScene))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function isPowerOf2(value) {
return (value & (value - 1)) == 0
}
Insert cell
function loadTexture (gl, img) {
// Obtain a texture resource from WebGL
const tex = gl.createTexture()
// Bind this texture to WebGL's 2D texture target, so we can write data into it
gl.bindTexture(gl.TEXTURE_2D, tex)
// Flip the texture upside down, since (0, 0) in WebGL is bottom-left and
// (0, 0) in image files is top-left
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
// Fill the texture with our loaded image data
// Arguments: target, level of detail, internal format, width, height, border, format, data type, data
// Level of detail: 0 = base image
// Internal format: RGB, RGBA, ALPHA, etc
// Border: always 0
// Format: must be same as internal format
// I skipped over the edge cases, you can read all about them (and more) in:
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D
// When using a DOM image element, which we do here, the width, height, and border arguments are implied,
// so we don't need to specify them.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)

if (isPowerOf2(img.width) && isPowerOf2(img.height)) {
// If the image's width and height are both powers of 2, we can enable mipmapping
// Mipmapping precomputes downscaled copies of the texture using an accurate but slow scaling algorithm
// When rendering the texture far away (i.e. smaller than its natural size),
// WebGL will automatically substitute one of the accurately-scaled copies instead of the original image
gl.generateMipmap(gl.TEXTURE_2D)
} else {
// WebGL 1 enforces certain restrictions on non-power-of-two (NPOT) textures

// If we read out of bounds of the texture (e.g. UV coords outside of
// [0, 1]) then clamp the coordinates to between [0, 1], don't try to
// repeat the texture
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

// Use linear coordinate interpolation rather than mipmapping, since we
// can't have mipmaps with NPOT textures. This is less accurate than mipmapping.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
}
// Return our WebGL texture resource
return tex
}
Insert cell
Insert cell
cubeTexture = loadTexture(gl, await FileAttachment('willie.jpg').image())
Insert cell
Insert cell
Insert cell
cubeVtxShaderSource2 = `
// Use high-precision floating-point values
precision highp float;

// Attributes are unique to each vertex
attribute vec3 position; // (X, Y, Z) coordinates in 3D space
attribute vec2 texCoord; // (U, V) [aka (S, T)] coordinates indicating which
// pixel of the texture is "glued" onto this vertex of
// the 3D surface

// Uniforms can be updated once before the shader is invoked
// (i.e. before a draw call is issued, with drawArrays/drawElements/etc)
uniform mat4 projMat; // Projects from 3D (perspective) into 2D
uniform mat4 modelViewMat; // Applies camera and model transformations

// Varyings are passed from the vertex shader to the fragment shader
// They must be declared in both shaders
varying vec2 vTexCoord;

void main(void) {
// Set varyings which will be used by fragment shader
// The GPU automatically interpolates these values between fragments
vTexCoord = texCoord;

// Multiply the input vertex position by matrices, to project it into 2D screen space
// Write our vertex position to the built-in output variable gl_Position
gl_Position = projMat * modelViewMat * vec4(position, 1.0);
}
`
Insert cell
Insert cell
cubeFragShaderSource2 = `
// Use high-precision floating-point values
precision highp float;

// ID of texture unit storing the texture we want to draw on this object
uniform sampler2D textureImg;

// Varyings are passed from the vertex shader to the fragment shader
// They must be declared in both shaders
varying vec2 vTexCoord;

void main(void) {
// Sample our texture at the coordinate defined by vTexCoord, and assign that
// to our output built-in variable gl_FragColor
gl_FragColor = texture2D(textureImg, vTexCoord);
}
`
Insert cell
Insert cell
shaderProgram2 = initShaderProgram(gl, cubeVtxShaderSource2, cubeFragShaderSource2)
Insert cell
shaderProgramInfo2 = ({
attribs: {
position: gl.getAttribLocation(shaderProgram2, 'position'),
texCoord: gl.getAttribLocation(shaderProgram2, 'texCoord'),
},
uniforms: {
projMat: gl.getUniformLocation(shaderProgram2, 'projMat'),
modelViewMat: gl.getUniformLocation(shaderProgram2, 'modelViewMat'),
textureImg: gl.getUniformLocation(shaderProgram2, 'textureImg'),
},
})
Insert cell
Insert cell
Insert cell
vtxTexCoordBuf = createBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(cubeVtxTexCoordArray), gl.STATIC_DRAW)
Insert cell
Insert cell
function drawScene2 () {
// It's considered best practice to schedule another animation frame
// immediately when the current frame begins
window.requestAnimationFrame(drawScene2)

// Call the function we declared earlier to animate the model matrix
animateModelMatrix()
// Multiply view matrix * model matrix and write result to modelViewMatrix
glMatrix.mat4.multiply(modelViewMat, viewMat, modelMat)

// Clear the screen to empty
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

// Use our shader program to draw this model
gl.useProgram(shaderProgram2)

// Pass our matrix data to the shader program
// 4fv = 4D array of 4D floating-point vectors
gl.uniformMatrix4fv(shaderProgramInfo2.uniforms.projMat, false, projMat)
gl.uniformMatrix4fv(shaderProgramInfo2.uniforms.modelViewMat, false, modelViewMat)
//
// NEW in Part 2!
//
// WebGL limits how many textures are available to a given draw call, usually
// around 16 on modern hardware. Available textures "slots" gl.TEXTURE0 - gl.TEXTURE[N]
// Let's use the first texture slot for our example here
gl.activeTexture(gl.TEXTURE0)
// Now that we've picked the appropriate texture slot, bind our texture to the
// 2D target of this texture slot
gl.bindTexture(gl.TEXTURE_2D, cubeTexture)
// Pass the texture slot ID (0) to the shader, so it knows from which texture to read
// 1i = one integer
gl.uniform1i(shaderProgramInfo2.uniforms.textureImg, 0)

// Bind our vertex position buffer
gl.bindBuffer(gl.ARRAY_BUFFER, vtxPosBuf)
// Specify layout of our vertex buffer.
// This flexibility offered by this function is most useful if you're using
// interleaved buffers, which we aren't.
//
// Arguments: attribute location, number of components per attribute, data type, normalized, stride
// Stride = "offset in bytes between the beginning of consecutive vertex attributes"
// 0 sets stride = number of components per attribute * component data type size
// Offset = "offset in bytes of the first component in the vertex attribute array"
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
gl.vertexAttribPointer(shaderProgramInfo2.attribs.position, 3, gl.FLOAT, false, 0, 0)
// Enable our attribute location (basically: we bound useful data to it and
// the shader is going to access it)
gl.enableVertexAttribArray(shaderProgramInfo2.attribs.position)
//
// NEW in Part 2!
//
gl.bindBuffer(gl.ARRAY_BUFFER, vtxTexCoordBuf)
// Second argument is 2 instead of 3 because texture coordinates are 2D vectors
gl.vertexAttribPointer(shaderProgramInfo2.attribs.texCoord, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(shaderProgramInfo2.attribs.texCoord)

// Bind our index buffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuf)
// Issue a draw call!
//
// A "draw call" is a special API call that actually draws content on the screen
// So far, we've spent hundreds of lines of code setting up the pipeline, but
// nothing has been rendered yet.
// This call tells WebGL that we're done configuring things and want to use the
// resources we've bound to produce some graphical output.
//
// Arguments: mode (primitive type), number of elements, element data type, offset
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawElements
gl.drawElements(gl.TRIANGLES, cubeIndexArray.length, gl.UNSIGNED_SHORT, 0)
}
Insert cell
Insert cell
(window.requestAnimationFrame(drawScene2))
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more