thor lee
thor lee

Reputation: 1

ThreeJS Add border for Text Object

Created Text with TextGeometry in Threejs,

    const font = await loadFont(fontUrl)
    const geometry = new TextGeometry("文字", {
        font: font,
        size: size,
        depth: 10,
        bevelEnabled: false,
        bevelThickness: 1,
        bevelSize: 1,
        bevelOffset: 0,
        bevelSegments: 0
    })
    const material = new THREE.MeshStandardMaterial({
        color: color
    })
   

    const mesh = new THREE.Mesh(geometry, material)

want to add a border for it, like this :

expected effect

blue in the middle is the original text, red is the border.

Try troika-three-text project. It's very nice, but seems no depth for text.

Upvotes: 0

Views: 53

Answers (1)

A_____ A_______
A_____ A_______

Reputation: 206

You can generate texture and create material for this object with texture: view result on codepen.io

  1. change text geometry UV (cuz it is a little strange and not in [0...1] bounds)
function flatUVbyXY(geometry) {
    if ( !(geometry.boundingBox instanceof THREE.Box3) ) {
        geometry.computeBoundingBox()
    }
    const bb = geometry.boundingBox.clone()
    const delta = new THREE.Vector3().subVectors(bb.max,bb.min)

    for (let i = 0,j = 0; i < geometry.attributes.position.array.length; i+=3, j+=2) {
        geometry.attributes.uv.array[j  ] = (geometry.attributes.position.array[i  ]-bb.min.x)/delta.x
        geometry.attributes.uv.array[j+1] = (geometry.attributes.position.array[i+1]-bb.min.y)/delta.y
    }
    geometry.attributes.uv.needsUpdate = true;
}
//flatUVbyXY(geometry)
//or
//flatUVbyXY(mesh.geometry)
  1. create texture with:
    2.1 make scene with only text where you want to add border and OrthographicCamera which see only text object
    2.2 render it and get result as texture
    2.3 place this texture on plane and render it with shaderMaterial which will add border with width you want (width is set in 3D space units)
    2.4 (optional) add update function which will update texture with new border width without doing all process again (cuz some parts is not needed to be done again)
function makeFlatBorder(geometry,borderWidth=5,fontColor=0x0000ff,borderColor=0xffff00,pxYSize = 200,canvas=document.createElement('canvas')) {
    if ( !(geometry.boundingBox instanceof THREE.Box3) ) {
        geometry.computeBoundingBox()
    }
    const bb = geometry.boundingBox.clone()
    const size = new THREE.Vector3().subVectors(bb.max,bb.min)
    const ortCam = new THREE.OrthographicCamera(
        bb.min.x,    bb.max.x,   //left, right
        bb.max.y,    bb.min.y,   //top , bottom
        bb.min.z-10, bb.max.z+10 //near, far 
    )

    const gl = canvas.getContext('webgl2')
    const maxTexSize = gl.getParameter(gl.MAX_TEXTURE_SIZE)
    
    const texSize = size.clone().divideScalar(size.y/pxYSize).round()
    if (texSize.x > maxTexSize || texSize.y > maxTexSize) {
        console.warn('you are trying to create too big texture (x,y):',texSize.x,texSize.y, '\nsize will be reduced')
        texSize.divideScalar((texSize.x > texSize.y ? texSize.x : texSize.y)/maxTexSize).floor()
        console.log('size of texture reduced to (x,y):',texSize.x,texSize.y)
    } else {
        console.log('texture size (x,y):',texSize.x,texSize.y)
    }
    
    const renderer = new THREE.WebGLRenderer({canvas: canvas});
    renderer.setSize( texSize.x, texSize.y );

    const scene0 = new THREE.Scene();
    const material = new THREE.MeshBasicMaterial({color: fontColor});
    material.depthTest = false;
    const text = new THREE.Mesh(geometry, material)
    scene0.add(text)
    renderer.render( scene0, ortCam );

    const addBorderInside = new THREE.ShaderMaterial({
        uniforms: {
            u_tex: {value: rgbaTextureFromCanvas(canvas)}, // result of rendering only text object with one color
            u_min: {value: bb.min},
            u_max: {value: bb.max},
            u_delta: {value: size},
            u_width: {value: borderWidth}, // in 3D space units
            u_targetColor: {value: new THREE.Color(fontColor)},
            u_backgroundColor: {value: new THREE.Color(0x00_00_00)},
            u_borderColor: {value: new THREE.Color(borderColor)},
        },
        vertexShader:`
            varying vec2 vUV;
            void main() {
                vUV = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }`,
        fragmentShader:`
            varying vec2 vUV;

            uniform sampler2D u_tex;
            
            uniform vec3 u_min;
            uniform vec3 u_max;
            uniform vec3 u_delta;

            uniform vec3 u_targetColor;
            uniform vec3 u_backgroundColor;
            uniform vec3 u_borderColor;

            uniform float u_width;

            vec4 getPx(sampler2D tex, vec2 pos, vec4 background) {
                if (pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0) {
                    return background;
                } else {
                    return texture2D( tex, pos );
                }
            }

            float minDistanceToBorder(sampler2D tex, vec2 pos, float limit, vec4 backgroundColor, vec4 targetColor) {
                vec4 thisColor = texture2D( tex, pos );
                if (all(equal(thisColor,backgroundColor))) {
                    return limit;
                }

                ivec2 iTextureSize = textureSize(tex,0);
                vec2 fTextureSize = vec2( float(iTextureSize.x) , float(iTextureSize.y) );
                vec2 fTexelSize = 1.0/fTextureSize;
                vec3 tempLimit = limit/u_delta*vec3(fTextureSize,0.0);
                ivec2 pxLimit = ivec2( floor(tempLimit.x), floor(tempLimit.y) );
                
                float result = limit;
                for (int y = -pxLimit.y; y <= pxLimit.y; y++) {
                    for (int x = -pxLimit.x; x <= pxLimit.x; x++) {
                        vec2 newPos = pos + fTexelSize*vec2( float(x), float(y) );
                        vec4 texel = getPx( tex, newPos, backgroundColor );
                        if (all(equal(texel,backgroundColor))) {
                            float dist = distance(pos*u_delta.xy,newPos*u_delta.xy);                       // rounded edges
                            // float dist = abs(pos.x-newPos.x)*u_delta.x + abs(pos.y-newPos.y)*u_delta.y; // sharp edges
                            if (dist < result) { return 0.0; }
                        }
                    }
                }
                return result;
            }       

            void main() {
                vec4 thisColor = texture2D( u_tex, vUV );

                vec3 color = thisColor.rgb;
                if (u_width > 0.0) {
                    if (all(equal(thisColor.rgb, u_backgroundColor))) {
                        color = u_borderColor;
                    } else {
                        float distanceToNearestBackgroundPx = minDistanceToBorder( u_tex, vUV, u_width, vec4(u_backgroundColor, 1.0), vec4(u_targetColor, 1.0) );
                        float val = distanceToNearestBackgroundPx/u_width;
                        color = u_borderColor*(1.0-val) + u_targetColor*val;                        
                    }
                }

                gl_FragColor = vec4(color,1.0);     
            }`
    });
    addBorderInside.needsUpdate = true;
    
    const scene1 = new THREE.Scene();
    const plane = new THREE.Mesh(new THREE.PlaneGeometry( size.x, size.y ), addBorderInside)
    plane.position.copy(new THREE.Vector3().lerpVectors(bb.max,bb.min,0.5))
    scene1.add(plane)
    renderer.render( scene1, ortCam );
    const resTexture = rgbaTextureFromCanvas(canvas) // result of applying shader on texture to add borders

    resTexture.newBorder = function (borderWidth) {
        console.time('update')
        addBorderInside.uniforms.u_width.value = borderWidth
        renderer.render( scene1, ortCam )
        const gl = canvas.getContext('webgl2')
        gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, this.source.data.data)
        this.needsUpdate = true;
        console.timeEnd('update')       
    }

    return resTexture
}

also you can chose edges type in GLSL shader (uncomment what you need)

float dist = distance(pos*u_delta.xy,newPos*u_delta.xy);                       // rounded edges
// float dist = abs(pos.x-newPos.x)*u_delta.x + abs(pos.y-newPos.y)*u_delta.y; // sharp edges

and finaly:

const loader = new FontLoader();
const fontUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/examples/fonts/helvetiker_bold.typeface.json';
const font = await fetch(fontUrl).then(r=>r.text().then(t=>loader.parse(JSON.parse(t))))

const symbolsPerLine = Math.round(Math.sqrt(Object.keys(font.data.glyphs).length/2.5)*2.5)
const allSymbols = Object.keys(font.data.glyphs).sort().reduce((ac,a,i)=>ac+(i%symbolsPerLine==0 && i!=0?'\n':'')+a,'')

const geometry = new TextGeometry(allSymbols, { // display all aviable in font symbols, symbol'ώ' in this font is a little broken 
    font: font,
    size: 80,
    depth: 10,
    bevelEnabled: false,
    bevelThickness: 1,
    bevelSize: 1,
    bevelOffset: 0,
    bevelSegments: 0
} );

flatUVbyXY(geometry)

const Tmaterial = new THREE.MeshBasicMaterial({
    map: makeFlatBorder(
         geometry // text geometry
        ,3        // borderWidth in 3D space units
        ,0x0000ff // fontColor
        ,0xffff00 // borderColor
        ,2048     // target texture height in pixels (if width will be bigger than gl.MAX_TEXTURE_SIZE size will be reduced to allowed bounds)
        ,document.querySelector('canvas[id=screen2]') // (optional) canvas in interface which show all texture
    )
    // ,side: THREE.DoubleSide
});
Tmaterial.map.magFilter = THREE.LinearFilter;
Tmaterial.map.needsUpdate = true

const mesh = new THREE.Mesh(geometry, Tmaterial)

scene.add(mesh)

renderer.render( scene, camera );   

result: view result on codepen.io result

Upvotes: 0

Related Questions