惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
C
CERT Recently Published Vulnerability Notes
Forbes - Security
Forbes - Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
LINUX DO - 最新话题
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
SecWiki News
SecWiki News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Last Watchdog
The Last Watchdog
S
Schneier on Security
T
Troy Hunt's Blog
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog
T
Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
A
Arctic Wolf
S
Secure Thoughts
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Latest
Security Latest
Scott Helme
Scott Helme
Security Archives - TechRepublic
Security Archives - TechRepublic
Latest news
Latest news
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
LINUX DO - 热门话题
P
Palo Alto Networks Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
Project Zero
Project Zero
V
Vulnerabilities – Threatpost
T
Threat Research - Cisco Blogs
Webroot Blog
Webroot Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Hacker News: Front Page
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog

WebGL Fundamentals

WebGL Using 2 or More Textures WebGL Implementing DrawImage WebGL 2D Matrices WebGL Implementing A Matrix Stack WebGL 2D Rotation WebGL 2D Scale WebGL 2D Translation WebGL - Rasterization vs 3D libraries WebGL 3D - Cameras WebGL 3D Geometry - Lathe WebGL 3D - Directional Lighting WebGL 3D - Point Lighting WebGL 3D - Normal Mapping WebGL 3D - Spot Lighting WebGL - Orthographic 3D WebGL 3D Perspective WebGL Textures WebGL and Alpha WebGL - Animation WebGL Anti-Patterns WebGL Attributes WebGL Boilerplate WebGL - Cross Origin Images WebGL Cross Platform Issues WebGL Cubemaps WebGL 3D - Data Textures WebGL - Drawing Multiple Things WebGL Drawing Without Data WebGL Environment Maps (reflections) WebGL Fog WebGL Framebuffers WebGL Fundamentals WebGL GPGPU WebGL How It Works WebGL Image Processing Continued WebGL Image Processing WebGL Indexed Vertices WebGL Optimization - Instanced Drawing WebGL - Less Code, More Fun WebGL Load Obj with Mtl WebGL Load Obj WebGL Matrices vs Math Matrices WebGL Multiple Views, Multiple Canvases WebGL Picking WebGL Planar and Perspective Projection Mapping WebGL Points, Lines, and Triangles WebGL Post Processing WebGL Precision Issues WebGL Pulling Vertices Accessing textures by pixel coordinate in WebGL2 A simple way to show the load on the GPU's vertex and fragment processing? Apply a displacement map and specular map Can anyone explain what this GLSL fragment shader is doing? Can I mute the warning about vertex attrib 0 being disabled? Create image warping effect in WebGL Creating a smudge/liquify effect How to draw Depth Sprites Determine min/max values for the entire image Don't blend a polygon that crosses itself Drawing 2D image with depth map to achieve pseudo-3D effect Drawing a heightmap Drawing layers with different points Drawing Many different models in a single draw call Drawing textured sprites with instanced drawing Efficient particle system in javascript? (WebGL) Emulating palette based graphics in WebGL FPS-like camera movement with basic matrix transformations Get the size of a point for collision checking GLSL shader to support coloring and texturing How can I compute for 500 points which of 1000 line segments is nearest to each point? How can I create a 16bit historgram of 16bit data How can I get all the uniforms and uniformBlocks How can I move the perspective vanishing point from the center of the canvas? How to Achieve Moving Line with Trail Effects How to bind an array of textures to a WebGL shader uniform? How to blend colors across 2 triangles How to combine more text drawing into fewer draw calls How to control the color between vertices How to create a torus How to detect clipped triangles in the framgment shader How to determine the average brightness in a scene? How to draw correctly textured trapezoid polygons How to fade the drawing buffer How to figure out how much GPU work to do without crashing WebGL How to get audio data into a shader How to get code completion for WebGL in Visual Studio Code How to get the 3d coordinates of a mouse click How to get pixelize effect in webgl? How to implement zoom from mouse in 2D WebGL How to import a heightmap in WebGL How to load images in the background with no jank How to make a smudge brush tool How to make WebGL canvas transparent How to optimize rendering a UI How to prevent texture bleeding with a texture atlas How to process particle positions How to read a single component with readPixels How to render large scale images like 32000x32000 How to simulate a 3D texture in WebGL How to support both WebGL and WebGL2
WebGL 3D Perspective Correct Texture Mapping
WebGLFundame · 2025-02-26 · via WebGL Fundamentals

This post is a continuation of a series of posts about WebGL. The first started with fundamentals. This article covers perspective correct texture mapping. To understand it you probably need to read up on perspective projection and maybe texturing as well. You also need to know about varyings and what they do but I'll cover them briefly here.

So in the "how it works" article we covered how varyings work. A vertex shader can declare a varying and set it to some value. Once the vertex shader has been called 3 times WebGL will draw a triangle. While it's drawing that triangle for every pixel it will call our fragment shader and ask it what color to make that pixel. Between the 3 vertices of the triangle it will pass us our varyings interpolated between the 3 values.

v_color is interpolated between v0, v1 and v2

Going back to our first article we drew a triangle in clip space, no math. We just passed in some clip space coordinates to a simple vertex shader that looked like this

  // an attribute will receive data from a buffer
  attribute vec4 a_position;

  // all shaders have a main function
  void main() {

    // gl_Position is a special variable a vertex shader
    // is responsible for setting
    gl_Position = a_position;
  }

We had a simple fragment shader that draws a constant color

  // fragment shaders don't have a default precision so we need
  // to pick one. mediump is a good default
  precision mediump float;

  void main() {
    // gl_FragColor is a special variable a fragment shader
    // is responsible for setting
    gl_FragColor = vec4(1, 0, 0.5, 1); // return reddish-purple
  }

So let's make that draw 2 rectangles in clip space. We'll pass it this data with X, Y, Z, and W for each vertex.

var positions = [
  -.8, -.8, 0, 1,  // 1st rect 1st triangle
   .8, -.8, 0, 1,
  -.8, -.2, 0, 1,
  -.8, -.2, 0, 1,  // 1st rect 2nd triangle
   .8, -.8, 0, 1,
   .8, -.2, 0, 1,

  -.8,  .2, 0, 1,  // 2nd rect 1st triangle
   .8,  .2, 0, 1,
  -.8,  .8, 0, 1,
  -.8,  .8, 0, 1,  // 2nd rect 2nd triangle
   .8,  .2, 0, 1,
   .8,  .8, 0, 1,
];

Here's that

Let's add a single varying float. We'll pass that directly from the vertex shader to the fragment shader.

  attribute vec4 a_position;
+  attribute float a_brightness;

+  varying float v_brightness;

  void main() {
    gl_Position = a_position;

+    // just pass the brightness on to the fragment shader
+    v_brightness = a_brightness;
  }

In the fragment shader we'll use that varying to set the color

  precision mediump float;

+  // passed in from the vertex shader and interpolated
+  varying float v_brightness;  

  void main() {
*    gl_FragColor = vec4(v_brightness, 0, 0, 1);  // reds
  }

We need to supply data for that varying so we'll make a buffer and put in some data. One value per vertex. We'll set all the brightness values for vertices on the left to 0 and those on the right to 1.

  // Create a buffer and put 12 brightness values in it
  var brightnessBuffer = gl.createBuffer();

  // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = brightnessBuffer)
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  var brightness = [
    0,  // 1st rect 1st triangle
    1, 
    0, 
    0,  // 1st rect 2nd triangle
    1, 
    1, 

    0,  // 2nd rect 1st triangle
    1, 
    0, 
    0,  // 2nd rect 2nd triangle
    1, 
    1, 
  ];

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(brightness), gl.STATIC_DRAW);

We also need to look up the location of the a_brightness attribute at init time

  // look up where the vertex data needs to go.
  var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
+  var brightnessAttributeLocation = gl.getAttribLocation(program, "a_brightness");

and setup that attribute at render time

  // Turn on the attribute
  gl.enableVertexAttribArray(brightnessAttributeLocation);

  // Bind the brightness buffer.
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  // Tell the attribute how to get data out of brightnessBuffer (ARRAY_BUFFER)
  var size = 1;          // 1 component per iteration
  var type = gl.FLOAT;   // the data is 32bit floats
  var normalize = false; // don't normalize the data
  var stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
  var offset = 0;        // start at the beginning of the buffer
  gl.vertexAttribPointer(
      brightnessAttributeLocation, size, type, normalize, stride, offset);

And now when we render we get two rectangles that are black on the left when brightness is 0 and red on the right when brightness is 1 and for the area in between brightness is interpolated or (varied) as it goes across the triangles.

So then, from the perspective article we know that WebGL takes whatever value we put in gl_Position and it divides it by gl_Position.w.

In the vertices above we supplied 1 for W but since we know WebGL will divide by W then we should be able do something like this and get the same result.

  var mult = 20;
  var positions = [
      -.8,  .8, 0, 1,  // 1st rect 1st triangle
       .8,  .8, 0, 1,
      -.8,  .2, 0, 1,
      -.8,  .2, 0, 1,  // 1st rect 2nd triangle
       .8,  .8, 0, 1,
       .8,  .2, 0, 1,

      -.8       , -.2       , 0,    1,  // 2nd rect 1st triangle
       .8 * mult, -.2 * mult, 0, mult,
      -.8       , -.8       , 0,    1,
      -.8       , -.8       , 0,    1,  // 2nd rect 2nd triangle
       .8 * mult, -.2 * mult, 0, mult,
       .8 * mult, -.8 * mult, 0, mult,
  ];

Above you can see that for every point on the right in the second rectangle we are multiplying X and Y by mult but, we are also setting W to mult. Since WebGL will divide by W we should get the exact same result right?

Well here's that

Note the 2 rectangles are drawn in the same place they were before. This proves X * MULT / MULT(W) is still just X and same for Y. But, the colors are different. What's going on?

It turns out WebGL uses W to implement perspective correct texture mapping or rather to do perspective correct interpolation of varyings.

In fact to make it easier to see let's hack the fragment shader to this

gl_FragColor = vec4(fract(v_brightness * 10.), 0, 0, 1);  // reds

multiplying v_brightness by 10 will make the value go from 0 to 10. fract will just keep the fractional part so it will go 0 to 1, 0 to 1, 0 to 1, 10 times

Now it should be easy to see the perspective.

A linear interpolation from one value to another would be this formula

 result = (1 - t) * a + t * b

Where t is a value from 0 to 1 representing some position between a and b. 0 at a and 1 at b.

For varyings though WebGL uses this formula

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

Where aW is the W that was set on gl_Position.w when the varying was as set to a and bW is the W that was set on gl_Position.w when the varying was set to b.

Why is that important? Well here's a simple textured cube like we ended up with in the article on textures. I've adjusted the UV coordinates to go from 0 to 1 on each side and it's using a 4x4 pixel texture.

Now let's take that example and change the vertex shader so that we divide by W ourselves. We just need to add 1 line.

attribute vec4 a_position;
attribute vec2 a_texcoord;

uniform mat4 u_matrix;

varying vec2 v_texcoord;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

+  // Manually divide by W.
+  gl_Position /= gl_Position.w;

  // Pass the texcoord to the fragment shader.
  v_texcoord = a_texcoord;
}

Dividing by W means gl_Position.w will end up being 1. X, Y, and Z will come out just like they would if we let WebGL do the division for us. Well here are the results.

We still get a 3D cube but the textures are getting warped. This is because by not passing W as it was before WebGL is not able to do perspective correct texture mapping. Or more correctly, WebGL is not able to do perspective correct interpolation of varyings.

If you recall W was our Z value from our perspective matrix. With W just being 1 WebGL just ends up doing a linear interpolation. In fact if you take the equation above

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

And change all the Ws to 1s we get

 result = (1 - t) * a / 1 + t * b / 1
          ---------------------------
             (1 - t) / 1 + t / 1

Dividing by 1 does nothing so we can simplify to this

 result = (1 - t) * a + t * b
          -------------------
             (1 - t) + t

(1 - t) + t is the same as 1. For example if t was .7 we'd get (1 - .7) + .7 which is .3 + .7 which is 1. In other words we can remove the bottom so we're left with

 result = (1 - t) * a + t * b

Which the same as the linear interpolation equation above.

Hopefully it's now clear why WebGL uses a 4x4 matrix and 4 value vectors with X, Y, Z, and W. X and Y divided by W get a clipspace coordinate. Z divided by W also get a clipspace coordinate in Z and W is still used during interopation of varyings and provides the ability to do perspective correct texture mapping.

Mid 1990s Game Consoles

As a little piece of trivia the PlayStation 1 and some of the other game consoles from the same era didn't do perspective correct texture mapping. Looking at the results above you can now see why they looked the way they did.