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

推荐订阅源

D
DataBreaches.Net
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Webroot Blog
Webroot Blog
AI
AI
P
Palo Alto Networks Blog
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
Spread Privacy
Spread Privacy
T
Tor Project blog
罗磊的独立博客
小众软件
小众软件
S
Security Affairs
酷 壳 – CoolShell
酷 壳 – CoolShell
量子位
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
NISL@THU
NISL@THU
博客园_首页
PCI Perspectives
PCI Perspectives
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
腾讯CDC
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
A
About on SuperTechFans
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
TaoSecurity Blog
TaoSecurity Blog
宝玉的分享
宝玉的分享
G
GRAHAM CLULEY
雷峰网
雷峰网
F
Full Disclosure
I
Intezer
Cloudbric
Cloudbric
博客园 - 三生石上(FineUI控件)
U
Unit 42

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 Correct Texture Mapping 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
WebGL Text - Textures
WebGLFundame · 2025-02-26 · via WebGL Fundamentals

This post is a continuation of many articles about WebGL. The last one was about using Canvas 2D for rendering text over a WebGL canvas. If you haven't read it you might want to check that out before continuing.

In the last article we went over how to use a 2D canvas to draw text over your WebGL scene. That technique works and is easy to do but it has a limitation that the text can not be obscured by other 3d objects. To do that we actually need to draw the text in WebGL.

The simplest way to do that is to make textures with text in them. You could for example go into Photoshop or some other paint program and draw an image with some text in it.

Then make some plane geometry and display it. This is actually how some games I've worked on did all their text. For example Locoroco only had about 270 strings. It was localized into 17 languages. We had an Excel sheet with all the languages and a script that would launch Photoshop and generate a texture, one for each message in each language.

Of course you can also generate the textures at runtime. Since WebGL is in the browser again we can rely on the Canvas 2D API to help generate our textures.

Starting with the examples from the previous article let's add a function to fill a 2D canvas with some text

var textCtx = document.createElement("canvas").getContext("2d");

// Puts text in center of canvas.
function makeTextCanvas(text, width, height) {
  textCtx.canvas.width  = width;
  textCtx.canvas.height = height;
  textCtx.font = "20px monospace";
  textCtx.textAlign = "center";
  textCtx.textBaseline = "middle";
  textCtx.fillStyle = "black";
  textCtx.clearRect(0, 0, textCtx.canvas.width, textCtx.canvas.height);
  textCtx.fillText(text, width / 2, height / 2);
  return textCtx.canvas;
}

Now that we need to draw 2 different things in WebGL, the 'F' and our text, I'm going to switch over to using some helper functions as described in a previous article. If it's not clear what programInfo, bufferInfo, etc are see that article.

So, let's create the 'F' and a unit quad.

// Create data for 'F'
var fBufferInfo = primitives.create3DFBufferInfo(gl);
// Create a unit quad for the 'text'
var textBufferInfo = primitives.createPlaneBufferInfo(gl, 1, 1, 1, 1, m4.xRotation(Math.PI / 2));

A unit quad is a quad (square) that's 1 unit big. This one is centered over the origin. createPlaneBufferInfo creates a plane in the xz plane. We pass in a matrix to rotate it and give us an xy plane unit quad.

Next create 2 shaders

// setup GLSL programs
var fProgramInfo = createProgramInfo(gl, ["vertex-shader-3d", "fragment-shader-3d"]);
var textProgramInfo = createProgramInfo(gl, ["text-vertex-shader", "text-fragment-shader"]);

And create our text texture

// create text texture.
var textCanvas = makeTextCanvas("Hello!", 100, 26);
var textWidth  = textCanvas.width;
var textHeight = textCanvas.height;
var textTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);
// make sure we can render it even if it's not a power of 2
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);

Setup uniforms for both the 'F' and text

var fUniforms = {
  u_matrix: m4.identity(),
};

var textUniforms = {
  u_matrix: m4.identity(),
  u_texture: textTex,
};

Now when we compute the matrices for the F we save off the F's view matrix

var fViewMatrix = m4.translate(viewMatrix,
    translation[0] + xx * spread, translation[1] + yy * spread, translation[2]);
fViewMatrix = m4.xRotate(fViewMatrix, rotation[0]);
fViewMatrix = m4.yRotate(fViewMatrix, rotation[1] + yy * xx * 0.2);
fViewMatrix = m4.zRotate(fViewMatrix, rotation[2] + now + (yy * 3 + xx) * 0.1);
fViewMatrix = m4.scale(fViewMatrix, scale[0], scale[1], scale[2]);
fViewMatrix = m4.translate(fViewMatrix, -50, -75, 0);

Drawing the F looks like this

gl.useProgram(fProgramInfo.program);

webglUtils.setBuffersAndAttributes(gl, fProgramInfo, fBufferInfo);

fUniforms.u_matrix = m4.multiply(projectionMatrix, fViewMatrix);

webglUtils.setUniforms(fProgramInfo, fUniforms);

// Draw the geometry.
gl.drawElements(gl.TRIANGLES, fBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);

For the text we just need the position of the origin of the F. We also need to scale our unit quad to match the dimensions of the texture. Finally we need to multiply by the projection matrix.

// use just the view position of the 'F' for the text
var textMatrix = m4.translate(projectionMatrix,
    fViewMatrix[12], fViewMatrix[13], fViewMatrix[14]);
// scale the quad to the size we need it.
textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1);

And then render the text

// setup to draw the text.
gl.useProgram(textProgramInfo.program);

webglUtils.setBuffersAndAttributes(gl, textProgramInfo, textBufferInfo);

m4.copy(textMatrix, textUniforms.u_matrix);
webglUtils.setUniforms(textProgramInfo, textUniforms);

// Draw the text.
gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);

So here it is

You'll notice that sometimes parts of our text cover up parts of our Fs. That's because we're drawing a quad. The default color of the canvas is transparent black (0,0,0,0) and we're drawing that color in the quad. We could instead blend our pixels.

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

This makes it take the source pixel (the color from our fragment shader) and combine it with the dest pixel (the color in the canvas) according to the blend function. We've set the blend function to SRC_ALPHA for source and ONE_MINUS_SRC_ALPHA for dest.

result = dest * (1 - src_alpha) + src * src_alpha

so for example if the dest is green 0,1,0,1 and the source is red 1,0,0,1 we'd have

src = [1, 0, 0, 1]
dst = [0, 1, 0, 1]
src_alpha = src[3]  // this is 1
result = dst * (1 - src_alpha) + src * src_alpha

// which is the same as
result = dst * 0 + src * 1

// which is the same as
result = src

For the parts of the texture with transparent black 0,0,0,0

src = [0, 0, 0, 0]
dst = [0, 1, 0, 1]
src_alpha = src[3]  // this is 0
result = dst * (1 - src_alpha) + src * src_alpha

// which is the same as
result = dst * 1 + src * 0

// which is the same as
result = dst

Here's the result with blending enabled.

You can see it's better but it's still not perfect. If you look close you'll sometimes see this issue

What's happening? We're currently drawing an F then its text, then the next F then its text repeated. We still have a depth buffer so when we draw the text for an F, even though blending made some pixels stay the background color the depth buffer was still updated. When we draw the next F if parts of that F are behind those pixels from some previously drawn text they won't be drawn.

We've just run into one of the most difficult issues of rendering 3D on a GPU. Transparency has issues.

The most common solution for pretty much all transparent rendering is to draw all the opaque stuff first, then after, draw all the transparent stuff sorted by z distance with the depth buffer testing on but depth buffer updating off.

Let's first separate drawing of the opaque stuff (the Fs) from the transparent stuff (the text). First we'll declare something to remember the text positions.

var textPositions = [];

And in the loop for rendering the Fs we'll remember those positions

var fViewMatrix = m4.translate(viewMatrix,
    translation[0] + xx * spread, translation[1] + yy * spread, translation[2]);
fViewMatrix = m4.xRotate(fViewMatrix, rotation[0]);
fViewMatrix = m4.yRotate(fViewMatrix, rotation[1] + yy * xx * 0.2);
fViewMatrix = m4.zRotate(fViewMatrix, rotation[2] + now + (yy * 3 + xx) * 0.1);
fViewMatrix = m4.scale(fViewMatrix, scale[0], scale[1], scale[2]);
fViewMatrix = m4.translate(fViewMatrix, -50, -75, 0);
+// Save the f's view position
+textPositions.push([fViewMatrix[12], fViewMatrix[13], fViewMatrix[14]]);

Before we draw the 'F's we'll disable blending and turn on writing to the depth buffer

gl.disable(gl.BLEND);
gl.depthMask(true);

For drawing the text we'll turn on blending and turn off writing to the depth buffer.

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.depthMask(false);

And then draw text at all the positions we saved

+// setup to draw the text.
+gl.useProgram(textProgramInfo.program);
+
+webglUtils.setBuffersAndAttributes(gl, textProgramInfo, textBufferInfo);

+textPositions.forEach(function(pos) {
  // draw the text

  // use just the view position of the 'F' for the text
*  var textMatrix = m4.translate(projectionMatrix, pos[0], pos[1], pos[2]);
  // scale the quad to the size we need it.
  textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1);

  m4.copy(textMatrix, textUniforms.u_matrix);
  webglUtils.setUniforms(textProgramInfo, textUniforms);

  // Draw the text.
  gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
+});

Note we moved setting the current program and the attributes outside the loop since we're drawing the same thing multiple times there's no reason to set those for each iteration.

And now it mostly works

Notice we didn't sort like I mentioned above. In this case since we're drawing mostly opaque text there's probably going to be no noticeable difference if we sort so I'll save that for some other article.

Another issue is the text is intersecting its own 'F'. There really isn't a specific solution for that. If you were making an MMO and wanted the text of each player to always appear you might try to make the text appear above the head. Just translate it +Y some number of units, enough to make sure it was always above the player.

You can also move it forward toward the camera. Let's do that here just for the hell of it. Because 'pos' is in view space that means it's relative to the eye (which is at 0,0,0 in view space). So if we normalize it we get a unit vector pointing from the eye to that point which we can then multiply by some amount to move the text a specific number of units toward or away from the eye.

+// because pos is in view space that means it's a vector from the eye to
+// some position. So translate along that vector back toward the eye some distance
+var fromEye = m4.normalize(pos);
+var amountToMoveTowardEye = 150;  // because the F is 150 units long
+var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye;
+var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye;
+var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye;
+var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ);

*var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ);
// scale the quad to the size we need it.
textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1);

Here's that.

You still might notice an issue with the edges of the letters.

The issue here is the Canvas 2D API produces only premultiplied alpha values. When we upload the contents of the canvas to a texture WebGL tries to un-premultiply the values but it can't do this perfectly because premultiplied alpha is lossy.

To fix that let's tell WebGL not to un-premultiply

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

This tells WebGL to supply premultiplied alpha values to gl.texImage2D and gl.texSubImage2D. If the data passed to gl.texImage2D is already premultiplied as it is for Canvas 2D data then WebGL can just pass it through.

We also need to change the blending function

-gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

The old one multiplied the src color by its alpha. That's what SRC_ALPHA means. But now our texture's data has already been multiplied by its alpha. That's what premultiplied means. So we don't need the GPU to do the multiplication. Setting it to ONE means multiply by 1.

The edges are gone now.

What if you want to keep the text a fixed size but still sort correctly? Well, if you remember from the perspective article our perspective matrix is going to scale our object by 1 / -Z to make it get smaller in the distance. So, we can just scale by -Z times some desired-scale to compensate.

...
// because pos is in view space that means it's a vector from the eye to
// some position. So translate along that vector back toward the eye some distance
var fromEye = normalize(pos);
var amountToMoveTowardEye = 150;  // because the F is 150 units long
var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye;
var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye;
var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye;
+var desiredTextScale = -1 / gl.canvas.height;  // 1x1 pixels
+var scale = viewZ * desiredTextScale;

var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ);
// scale the quad to the size we need it.
*textMatrix = m4.scale(textMatrix, textWidth * scale, textHeight * scale, 1);
...

If you want to draw different text at each F you should make a new texture for each F and just update the text uniforms for that F.

// create text textures, one for each F
var textTextures = [
  "anna",   // 0
  "colin",  // 1
  "james",  // 2
  "danny",  // 3
  "kalin",  // 4
  "hiro",   // 5
  "eddie",  // 6
  "shu",    // 7
  "brian",  // 8
  "tami",   // 9
  "rick",   // 10
  "gene",   // 11
  "natalie",// 12,
  "evan",   // 13,
  "sakura", // 14,
  "kai",    // 15,
].map(function(name) {
  var textCanvas = makeTextCanvas(name, 100, 26);
  var textWidth  = textCanvas.width;
  var textHeight = textCanvas.height;
  var textTex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, textTex);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);
  // make sure we can render it even if it's not a power of 2
  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);
  return {
    texture: textTex,
    width: textWidth,
    height: textHeight,
  };
});

Then at render time select a texture

*textPositions.forEach(function(pos, ndx) {

  +// select a texture
  +var tex = textTextures[ndx];

  // scale the F to the size we need it.
  var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ);
  // scale the quad to the size we need it.
  *textMatrix = m4.scale(textMatrix, tex.width * scale, tex.height * scale, 1);

and set the uniform for the texture before drawing

  *textUniforms.u_texture = tex.texture;

We've been using black to draw the text into the canvas. It would be more useful if we rendered the text in white. Then we could multiply the text by a color and make it any color we want.

First we'll change the text shader to multiply by a color

varying vec2 v_texcoord;

uniform sampler2D u_texture;
+uniform vec4 u_color;

void main() {
*   gl_FragColor = texture2D(u_texture, v_texcoord) * u_color;
}

And when we draw the text into the canvas use white

textCtx.fillStyle = "white";

Then we'll make some colors

// colors, 1 for each F
var colors = [
  [0.0, 0.0, 0.0, 1], // 0
  [1.0, 0.0, 0.0, 1], // 1
  [0.0, 1.0, 0.0, 1], // 2
  [1.0, 1.0, 0.0, 1], // 3
  [0.0, 0.0, 1.0, 1], // 4
  [1.0, 0.0, 1.0, 1], // 5
  [0.0, 1.0, 1.0, 1], // 6
  [0.5, 0.5, 0.5, 1], // 7
  [0.5, 0.0, 0.0, 1], // 8
  [0.0, 0.0, 0.0, 1], // 9
  [0.5, 5.0, 0.0, 1], // 10
  [0.0, 5.0, 0.0, 1], // 11
  [0.5, 0.0, 5.0, 1], // 12,
  [0.0, 0.0, 5.0, 1], // 13,
  [0.5, 5.0, 5.0, 1], // 14,
  [0.0, 5.0, 5.0, 1], // 15,
];

At draw time we select a color

// set color uniform
textUniforms.u_color = colors[ndx];

Colors

This technique is actually the technique most browsers use when they are GPU accelerated. They generate textures with your HTML content and all the various styles you've applied and as long as that content doesn't change they can just render the texture again when you scroll etc.. Of course if you're updating things all the time then this technique might get a little bit slow because re-generating the textures and re-uploading them to the GPU is a relatively slow operation.

In the next article we'll go over a technique that is probably better for cases where things update often.

Scaling Text without pixelation

You might notice in the examples before we started using a consistent size the text gets very pixelated as it gets close to the camera. How do we fix that?

Well, honestly it's not very common to scale 2D text in 3D. Look at most games or 3D editors and you'll see the text is almost always one consistent size regardless of how far or close to the camera it is. In fact often that text might be drawn in 2D instead of 3D so that even if someone or something is behind something else like a teammate behind a wall you can still read the text.

If you do happen to want to scale 2D text in 3D I don't know of any easy options. A few off the top of my head

  • Make different sizes of textures with fonts at different resolutions. You then use the higher resolution textures as the text gets larger. This is called LODing (using different Levels of Detail).
  • Another would be to render the textures with the exact correct size of text every frame. That would likely be really slow.
  • Yet another would be to make 2D text out of geometry. In other words instead of drawing text into a texture make text from lots and lots of triangles. That works but it has other issues in that small text will not render well and large text you'll start to see the triangles.
  • One more is to use very special shaders that render curves. That's very cool but way beyond what I can explain here.