ThreeJS 为文本对象添加边框

问题描述 投票:0回答:1

在 Threejs 中使用 TextGeometry 创建文本,

    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)

想为其添加边框,如下所示:

预期效果

中间蓝色的是原文,红色是边框。

尝试三驾马车三文本项目。 非常好,但文字似乎没有深度。

javascript text three.js 3d threejs-editor
1个回答
0
投票

您可以生成纹理并为该带有纹理的对象创建材质: 在 codepen.io 上查看结果

  1. 更改文本几何 UV(因为它有点奇怪并且不在 [0...1] 范围内)
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. 创建纹理:
    2.1 创建仅包含文本的场景,您要在其中添加边框和仅看到文本对象的正交相机
    2.2 渲染它并得到纹理结果
    2.3将此纹理放置在平面上并使用shaderMaterial渲染它,这将添加您想要的宽度的边框(宽度以3D空间单位设置)
    2.4(可选)添加更新功能,该功能将使用新的边框宽度更新纹理,而无需再次执行所有过程(因为某些部分不需要再次执行)
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;

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

                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
}

您也可以在 GLSL 着色器中选择边缘类型(取消注释您需要的内容)

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

最后:

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 );   

结果: 在 codepen.io 上查看结果 result

© www.soinside.com 2019 - 2024. All rights reserved.