Use three.js to create text that does not change in size with the scene

Posted by dreamer on Tue, 03 Dec 2019 18:41:50 +0100

Using three.js to create text whose size does not change with the scene requires the following two steps:

1. Draw the text on the canvas.

2. Create a shader material and put the text in the 3D scene.

Advantage:

1. Compared with html, these texts can be occluded by the model and have more 3D effect.

2. It does not change the size with the rotation and scale of the scene. It is suitable for 3D annotation because it does not have the situation that you cannot see clearly in the distance.

Design sketch:

 

Example code 1: https://github.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/object/text/UnscaledText.js

Example code 2: https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/object/text/UnscaledText.js

 

Implementation method

 

1. Use canvas to draw the text, first draw the edge with black, then draw the text with white. Black stroke is mainly used to make the text clear on the white background.

 

let context = canvas.getContext('2d');

context.imageSmoothingQuality = 'high';
context.textBaseline = 'middle';
context.textAlign = 'center';
context.lineWidth = 4;

let halfWidth = canvas.width / 2;
let halfHeight = canvas.height / 2;

// Drawing stroke
context.font = `16px "Microsoft YaHei"`;
context.strokeStyle = '#000';
context.strokeText(text, halfWidth, halfHeight);

// Draw text
context.fillStyle = '#fff';
context.fillText(text, halfWidth, halfHeight);

 

 

2. Create a shader material, face the text to the screen, and render it into a 3D scene.

 

let geometry = new THREE.PlaneBufferGeometry();
let material = new THREE.ShaderMaterial({
    vertexShader: UnscaledTextVertexShader,
    fragmentShader: UnscaledTextFragmentShader,
    uniforms: {
        tDiffuse: {
            value: new THREE.CanvasTexture(canvas)
        },
        width: {
            value: canvas.width
        },
        height: {
            value: canvas.height
        },
        domWidth: {
            value: renderer.domElement.width
        },
        domHeight: {
            value: renderer.domElement.height
        }
    },
    transparent: true
});

let mesh = new THREE.Mesh(geometry, material);

 

Note: because the text edge painted on canvas is translucent, the material should be set to translucent to achieve the text edge smoothing effect.

 
UnscaledTextVertexShader
 
precision highp float;

uniform float width;
uniform float height;
uniform float domWidth;
uniform float domHeight;

varying vec2 vUv;
 
void main() {
    vUv = uv;
    vec4 proj = projectionMatrix * modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
    gl_Position = vec4(
        proj.x / proj.w  + position.x * width / domWidth * 2.0,
        proj.y / proj.w + position.y * height / domHeight * 2.0,
        proj.z / proj.w,
        1.0
    );
}

 

Explain:

a, (0.0, 0.0, 0.0) is the world coordinate of the plane center. Multiply the modelViewMatrix and projectionMatrix to the left to get the coordinates in the screen coordinate system.

b. proj.x / proj.w + position.x * width / domWidth * 2.0 means to put the center of the flat panel in the correct position of the world coordinate system, so that the width of the flat panel display is exactly equal to the number of pixels on the screen, avoiding text scaling.

c. multiply by 2.0 because the width and height of the panel generated by three.js by default is 1, and the width and height of the screen coordinate system are from - 1 to 1, which is 2.

When d, gl_Position.w is 1.0, it is a positive projection, and the size of the model does not change with the depth of the screen.

 
UnscaledTextFragmentShader
 
precision highp float;

uniform sampler2D tDiffuse;
uniform float width;
uniform float height;
 
varying vec2 vUv;
 
void main() {
    // Be careful vUv Be sure to take the color from the integer coordinates of the canvas, otherwise the text will be blurred.
    vec2 _uv = vec2(
        (floor(vUv.s * width) + 0.5) / width,
        (floor(vUv.t * height) + 0.5) / height
    );

    gl_FragColor = texture2D( tDiffuse, _uv );
}

 

Explain:

1. The uv coordinate must exactly correspond to the pixel points on the canvas, otherwise the text will be blurred.

 

The solution of text blur

When using three.js or WebGL to draw text, it is easy to encounter the problem of text blur. There are several main reasons.
 
 
1. The line drawn on canvas is drawn from the center of two pixels.
 
In fact, there are 0.5px translucent lines on both sides of 1px line, and 2px is actually drawn. When drawing, be sure to start from (integer + 0.5px) pixels.
 
For details, please refer to canvas to solve the problem of 1px line blur: https://www.jianshu.com/p/c0970eecd843
 
In the above code, the font size and line width are even, so there is no such problem.
 
 
2. When selecting colors from the map according to uv coordinates, it is necessary to take the integer pixels on the map exactly, otherwise color interpolation will be carried out, resulting in blur.
 
I've been stuck with this question for a long time. The specific phenomenon is that with the change of perspective, the words are sometimes clear, sometimes vague, and flash.
 
The solution is to "round" the auto interpolated uv coordinates in the chip source shader, just to (integer + 0.5 pixels). Why add 0.5? See the above article "canvas solves the problem of 1px line blur".
 
Implementation code:
 
vec2 _uv = vec2(
    (floor(vUv.s * width) + 0.5) / width,
    (floor(vUv.t * height) + 0.5) / height
);

 

Where width and height are the width and height of the map, respectively.
 
3. GL ﹣ position.xy exactly corresponds to the pixel points on the screen.
 
This is one of the reasons I guess. After the modification according to reason 2, the text is not fuzzy. So, this one wasn't tested carefully.
 
 

Reference material

1. Open source 3D scene editor based on three.js: https://github.com/tengge1/ShadowEditor
2. canvas solves the problem of 1px line blur: https://www.jianshu.com/p/c0970eecd843
 
 
 

Topics: Javascript github