WebGL and OpenGL Differences

From WebGL Public Wiki
Jump to navigation Jump to search

WebGL is based on the OpenGL ES 2.0 specification, and retains the semantics of OpenGL ES in order to maximize portability to mobile devices. There are some significant differences in behavior of similar APIs between OpenGL ES 2.0 and the OpenGL API on desktop systems. Many OpenGL programmers are familiar with the semantics on the desktop, and may not know about these differences in behavior. We highlight them here for clarity.

Non-Power of Two Texture Support

While OpenGL 2.0 and later for the desktop offer full support for non-power-of-two (NPOT) textures, OpenGL ES 2.0 and WebGL have only limited NPOT support. The restrictions are defined in Sections 3.8.2, "Shader Execution", and 3.7.11, "Mipmap Generation", of the OpenGL ES 2.0 specification, and are summarized here:

  • generateMipmap(target) generates an INVALID_OPERATION error if the level 0 image of the texture currently bound to target has an NPOT width or height.
  • Sampling an NPOT texture in a shader will produce the RGBA color (0, 0, 0, 1) if:
    • The minification filter is set to anything but NEAREST or LINEAR: in other words, if it uses one of the mipmapped filters.
    • The repeat mode is set to anything but CLAMP_TO_EDGE; repeating NPOT textures are not supported.

If your application doesn't require the REPEAT wrap mode, and can tolerate the lack of mipmaps, then you can simply configure the WebGLTexture object appropriately at creation time:

var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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);

However, if your application requires the REPEAT wrap mode for correctness, you can easily resize the image to the next largest power of two dimensions using DOM APIs. Here is an example of how to do this. image is an HTML image object that has been fully loaded; its onload handler has already been called.

function createTextureFromImage(image) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    if (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height)) {
        // Scale up the texture to the next highest power of two dimensions.
        var canvas = document.createElement("canvas");
        canvas.width = nextHighestPowerOfTwo(image.width);
        canvas.height = nextHighestPowerOfTwo(image.height);
        var ctx = canvas.getContext("2d");
        ctx.drawImage(image, 0, 0, image.width, image.height);
        image = canvas;
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.bindTexture(gl.TEXTURE_2D, null);
    return texture;

function isPowerOfTwo(x) {
    return (x & (x - 1)) == 0;

function nextHighestPowerOfTwo(x) {
    for (var i = 1; i < 32; i <<= 1) {
        x = x | x >> i;
    return x + 1;

If your application allows, you can also resize your source images offline.

Vertex Attribute 0

On desktop GL, vertex attribute 0 has special semantics. First, it must be enabled as an array, or no geometry will be drawn. Second, it does not have persistent state, so calling glGetVertexAttribfv(0, GL_CURRENT_VERTEX_ATTRIB, ...) generates an OpenGL error.

On OpenGL ES 2.0, vertex attribute 0 has no special semantics.

WebGL follows the OpenGL ES 2.0 convention; all vertex attributes behave identically. This requires implementations on desktop GL to perform a certain amount of emulation, but this was considered to be a small price to pay for consistent behavior.

No double-precision floating-point support

OpenGL ES 2.0 does not support the GL_DOUBLE data type, either for vertex attributes or texture data. This means that the Typed Array view type Float64Array is not currently useful in conjunction with the WebGL API.

No 3D Texture support

OpenGL ES 2.0 does not support 3D textures. It is possible to emulate 3D textures with 2D textures.

// tex is a texture with each slice of the cube placed horizontally across the texture.
// texCoord is a 3d texture coord
// size is the size if the cube in pixels.

vec4 sampleAs3DTexture(sampler2D tex, vec3 texCoord, float size) {
   float sliceSize = 1.0 / size;                         // space of 1 slice
   float slicePixelSize = sliceSize / size;              // space of 1 pixel
   float sliceInnerSize = slicePixelSize * (size - 1.0); // space of size pixels
   float zSlice0 = min(floor(texCoord.z * size), size - 1.0);
   float zSlice1 = min(zSlice0 + 1.0, size - 1.0);
   float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize;
   float s0 = xOffset + (zSlice0 * sliceSize);
   float s1 = xOffset + (zSlice1 * sliceSize);
   vec4 slice0Color = texture2D(tex, vec2(s0, texCoord.y));
   vec4 slice1Color = texture2D(tex, vec2(s1, texCoord.y));
   float zOffset = mod(texCoord.z * size, 1.0);
   return mix(slice0Color, slice1Color, zOffset);

You can see this in use in this sample. There's an explanation of how it works here.

Here's another where each slice is placed in a grid. For example an 8x8x8 cube might be a texture that is 4x2 slices, each slice 8x8 pixels

// tex is a texture with each slice of the cube placed in grid in a texture.
// texCoord is a 3d texture coord
// size is the size if the cube in pixels.
// slicesPerRow is how many slices there are across the texture
// numRows is the number of rows of slices

vec2 computeSliceOffset(float slice, float slicesPerRow, vec2 sliceSize) {
  return sliceSize * vec2(mod(slice, slicesPerRow), 
                          floor(slice / slicesPerRow));

vec4 sampleAs3DTexture(
    sampler2D tex, vec3 texCoord, float size, float numRows, float slicesPerRow) {
  float slice   = texCoord.z * size;
  float sliceZ  = floor(slice);                         // slice we need
  float zOffset = fract(slice);                         // dist between slices

  vec2 sliceSize = vec2(1.0 / slicesPerRow,             // u space of 1 slice
                        1.0 / numRows);                 // v space of 1 slice

  vec2 slice0Offset = computeSliceOffset(sliceZ, slicesPerRow, sliceSize);
  vec2 slice1Offset = computeSliceOffset(sliceZ + 1.0, slicesPerRow, sliceSize);

  vec2 slicePixelSize = sliceSize / size;               // space of 1 pixel
  vec2 sliceInnerSize = slicePixelSize * (size - 1.0);  // space of size pixels

  vec2 uv = slicePixelSize * 0.5 + texCoord.xy * sliceInnerSize;
  vec4 slice0Color = texture2D(tex, slice0Offset + uv);
  vec4 slice1Color = texture2D(tex, slice1Offset + uv);
  return mix(slice0Color, slice1Color, zOffset);
  return slice0Color;

See example here


GLSL texture functions that end in "Lod" (eg texture2DLod) are only permitted in the vertex shader.

Precision Qualifiers

Precision qualifiers for variables in shaders are significant in GLSL ES. In contrast, on desktop systems, the maximum precision is often used even for lower-precision variables. This implies that the effects of precision qualifiers can often only be seen by testing content on mobile devices.

Per the OpenGL ES 2.0 specification, the built-in macro GL_FRAGMENT_PRECISION_HIGH is defined to one on systems supporting highp precision in the fragment language. Developers wishing to use the highest available precision in their fragment shaders are encouraged to write:

precision highp float;
precision mediump float;

Otherwise, if the highest precision is not absolutely required, then this unconditional declaration in the fragment shader:

precision mediump float;

will achieve the best portability. Testing should still be done on mobile devices to ensure no rendering artifacts are visible because of the use of lower precision than on desktop systems.