在 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)
想为其添加边框,如下所示:
中间蓝色的是原文,红色是边框。
尝试三驾马车三文本项目。 非常好,但文字似乎没有深度。
您可以生成纹理并为该带有纹理的对象创建材质: 在 codepen.io 上查看结果
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)
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 );