Merging multiple TextBufferGeometries in a scene - three.js

I'm trying to change the following code to use TextBufferGeometry instead of TextGeometry as I feel like it may improve the performance in my use case. The code below works for rendering multiple text elements into my scene however when I change..
let geometry = new THREE.TextGeometry(... to let geometry = new THREE.TextBufferGeometry(...
this code no longer renders text into the scene. I'm unsure of what needs changing in order to make use of TextBufferGeometry
const materialWhite = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide });
const textArray = [
{ text: `Message 12345`, zDistance: 100 },
{ text: `Message 67890`, zDistance: 200 },
{ text: `Message 13579`, zDistance: 300 },
];
var singleFontGeometry = new THREE.Geometry();
for (let index = 0; index < textArray.length; index++) {
loaderFonts.load('./assets/fonts/Oxanium.json', function(font) {
let geometry = new THREE.TextGeometry(`${textArray[index].text}`, {
font: font,
size: 20,
height: 1
});
let mesh = new THREE.Mesh(geometry, materialWhite);
mesh.position.set(100, 100, textArray[index].zDistance);
singleFontGeometry.mergeMesh(mesh);
});
}
var meshFont = new THREE.Mesh(singleFontGeometry, materialWhite);
this.scene.add(meshFont);

The code above will require three main changes:
In the loop, create THREE.TextBufferGeometry instances instead of TextGeometry.
Instead of setting a position on each mesh, bake that transform into the geometry before merging, with geometry.translate(...).
Instead of adding geometries to a THREE.Geometry instance, create a merged BufferGeometry instance using BufferGeometryUtils. BufferGeometry does not have a .mergeMesh() method, and .merge() overwrites vertices rather than creating a union, so neither are available here.
Also, but somewhat unrelated to the question, you probably don't want to load the font inside your For-loop, to avoid unnecessary requests. With these changes, the code should look something like this:
import * as THREE from 'three';
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
var mergedGeometry;
// Load the font once.
loaderFonts.load('./assets/fonts/Oxanium.json', function(font) {
// Then create a *BufferGeometry for each item of text.
var geometries = textArray.map(function(text) {
var geometry = new THREE.TextBufferGeometry(text.text, {
font: font,
size: 20,
height: 1
});
// Take each item's unique Z offset, and bake it into the geometry.
geometry.translate(0, 0, text.zDistance);
return geometry;
});
// Merge all text geometries.
mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries );
var mesh = new THREE.Mesh(mergedGeometry, materialWhite);
// Put the shared x=100,y=100 offset onto the mesh, so it can be changed later.
mesh.position.set(100, 100, 0);
scene.add(mesh);
});
^Note that BufferGeometryUtils is not included in the three.js build itself, and must be added to your application separately, like loaders, controls, and other things in the examples/js* folder.

Related

How to control size and color of textGeometry with dat.gui?

I found out that I could use dat.gui to control size and color of textGeometry besides changing color and size for every other scene through editing .js file. But probably bad architecture of my code, I am not able to control or even add gui folder to the scene. It probably has something to do with FontLoader that I'm using.
I tried placing dat.gui inside and outside my textGeometry creation function, none of them worked. As far as I understood, every time size or color changes it should dispose and remove the created mesh to create a new one with the new color/size parameters (i also update for every each keydown event to create a new mesh so that's my thought).
textGeometry, textMesh, textMaterial etc. are defined in global
function textCreation() {
const fontLoader = new FontLoader();
fontLoader.load(
"node_modules/three/examples/fonts/droid/droid_sans_bold.typeface.json",
(droidFont) => {
textGeometry = new TextGeometry(initText, { //initText variable gets updated with each keydown
size: 5,
height: 2,
font: droidFont,
parameters: {
size: 5,
height: 2,
},
});
textMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
textMesh = new THREE.Mesh(textGeometry, textMaterial);
group = new THREE.Group();
group.add(textMesh);
scene.add(group);
}
And here is the dat.gui controller that I tried to place in and out of this function.
let textFolder = gui.addFolder("Text Controls");
textFolder.add(textGeometry, "size", 10, 20).onChange(function () {
textGeometry.setSize(textGeometry.size);
// Probably dispose and remove old mesh from scene and create mesh with these new parameters
});
textFolder.addColor(textGeometry, "color").onChange(function () {
textGeometry.setColor(textGeometry.color);
I couldn't manage to add ANY dat.gui controller without breaking the scene. By the way I'm kinda new to JavaScript and three.JS so further explanations are welcome if there are any.
textGeometry.setSize(textGeometry.size);
This does not work since there isn't a setSize() method. You have to call dispose() on the existing geometry and then create a new one based on your updated parameters. You then assign the new geometry to your text mesh. So something like:
const params = {
color: '#ffffff',
size: 5
};
textFolder.add(params, "size", 10, 20).onChange(function (value) {
textGeometry.dispose();
textGeometry = new TextGeometry(initText, {
size: value,
height: 2,
font: droidFont
});
textMesh.geometry = textGeometry;
});
The material's color can be changed without creating a new object.
textFolder.addColor(params, 'color').onChange(function (value) {
textMaterial.color.set(value);
});

THREE.JS postprocessing: Duplicating postprocessed shader material to render properly on different meshes in the same scene

Think of my question as a more complicated version of this question here:
Three.js, sharing ShaderMaterial between meshes but with different uniform sets
tl;dr: I'm relying on a skin shader from THREE.js r100 to enable subsurface scattering on the meshes to make them appear more realistic but only one of them actually has the proper postprocessing to enable the effects.
The postprocessing effect is desired on all three meshes but only exists on one, the center. You can tell because the lighting looks right, and you can see where light travels "through" the mesh in thinner areas, like on the bottom part of the neck area and the ears (and if you get reaaaallly close it's in the nose too :D ).
Where the code is hosted has some wild things going on to generate the mesh, but that's beside the point, the main problem that I'm having is what you can see, only the center mesh (the one that was first added to the scene) actually has the proper effects applied to its material, the others have cloned versions of that shader but they don't render with the proper post-processing, I have a feeling it's because they're sharing uniforms or something related to that, but I'm not sure how to duplicate it properly. I was wondering how I can fix it? Do I add more render passes?
Is the way to adjust the render passes for the shader materials by adding even more passes relevant to the materials or just merely editing the uniforms (as stated in the linked question)?
I'm a bit lost and I've tried a lot to get this to work (though I'm definitely new to THREE.js post-processing passes), but you're my last hope. To someone experienced with this I feel like the solution will be very straightforward, I have a feeling I'm missing something very basic.
to view the output: https://abrasive-likeable-gateway.glitch.me/index-shader.html
to view the code: https://glitch.com/edit/#!/abrasive-likeable-gateway?path=index-shader.html%3A655%3A0
the filesystem is visible on the left side, you can remix the project on glitch (w/ a free account) to edit the code. I would've used codepen but I didn't want to deal with linking all of the three.js dependencies.
at index-shader.html on line 655 is where the setup begins for postprocessing
and at SS-xfer-placement.js on line 2838 is where rendering happens
in the same document, between lines 1900 - 2048 is the following code, which I suspect is where things are wrong, it looks like this, and imports the mesh, adds a material to it (that was set up in the html file after line 655) and adds it to the scene
the code looks like this:
setTimeout(()=>{
updateFaceParams()
setTimeout(()=>{
console.log(scene)
//send the model from ONE to THREE, init THREE
// document.querySelector("#ONEJS").style.height = "10vh!important;"
// document.querySelector("#ONEJS").style.width = "10vw!important;"
document.querySelector("#ONEJS").style.position = `absolute`
document.querySelector("#ONEJS").style.top = `0px`
document.querySelector("#ONEJS").style.right = `0px`
initTHREE();
let materialClone = window.THREEmaterial.clone()
var facePortion = scene.getObjectByName("face").geometry
console.log("FACE", scene.getObjectByName("face"))
var geometryConvert = convertGeometryToFiveBufferGeometry(facePortion)
var transferMesh = new THREE.Mesh( geometryConvert, window.THREEmaterial );
transferMesh.position.y = - 50;
transferMesh.rotation.y=3*Math.PI
// transferMesh.scale.set( scale, scale, scale );
transferMesh.scale.set(200,200,200)
transferMesh.doubleSided = true;
// console.log(transferMesh)
transferMesh.name = "face"
transferMesh.rotateY(Math.PI/180 * 180);
transferMesh.material.flatShading = false
transferMesh.material.shading = THREE.SmoothShading
THREEscene.add( transferMesh );
// console.log("test",transferMesh)
// console.log(THREEscene)
//
}, 1000)
setTimeout(()=>{
updateFaceParams()
setTimeout(()=>{
var facePortion = scene.getObjectByName("face").geometry
console.log("FACE", scene.getObjectByName("face"))
var geometryConvert = convertGeometryToFiveBufferGeometry(facePortion)
var transferMesh = new THREE.Mesh( geometryConvert, window.THREEmaterial.clone() );
transferMesh.position.y = - 50;
transferMesh.rotation.y=3*Math.PI
// transferMesh.scale.set( scale, scale, scale );
transferMesh.scale.set(200,200,200)
transferMesh.doubleSided = true;
// console.log(transferMesh)
transferMesh.name = "face"
transferMesh.rotateY(Math.PI/180 * 180);
transferMesh.material.flatShading = false
transferMesh.material.shading = THREE.SmoothShading
transferMesh.position.set(transferMesh.position.x+200, transferMesh.position.y, transferMesh.position.z)
THREEscene.add( transferMesh );
var THREErenderModelUV = new THREE.RenderPass( THREEscene,THREEcamera, window.THREEmaterialUV.clone(), new THREE.Color( 0x575757 ) );
THREEcomposer.addPass( THREErenderModelUV );
//TODO: write a stack overflow question about copying shaders!!!
setTimeout(()=>{
updateFaceParams()
setTimeout(()=>{
var facePortion = scene.getObjectByName("face").geometry
console.log("FACE", scene.getObjectByName("face"))
var geometryConvert = convertGeometryToFiveBufferGeometry(facePortion)
var transferMesh = new THREE.Mesh( geometryConvert, window.THREEmaterial.clone() );
// \var transferMesh = new THREE.Mesh( geometryConvert, new THREE.MeshPhongMaterial({color:0xffffff}) );
transferMesh.position.y = - 50;
transferMesh.rotation.y=3*Math.PI
// transferMesh.scale.set( scale, scale, scale );
transferMesh.scale.set(200,200,200)
transferMesh.doubleSided = true;
// console.log(transferMesh)
transferMesh.name = "face"
transferMesh.rotateY(Math.PI/180 * 180);
transferMesh.material.flatShading = false
transferMesh.material.shading = THREE.SmoothShading
transferMesh.position.set(transferMesh.position.x-200, transferMesh.position.y, transferMesh.position.z)
THREEscene.add( transferMesh );
// var THREErenderModelUV = new THREE.RenderPass( THREEscene,THREEcamera, window.THREEmaterialUV.clone(), new THREE.Color( 0x575757 ) );
// THREEcomposer.addPass( THREErenderModelUV );
var THREErenderModelUV = new THREE.RenderPass( THREEscene,THREEcamera, THREEmaterialUV.clone(), new THREE.Color( 0x575757 ) );
// var THREEeffectCopy = new THREE.ShaderPass( THREE.CopyShader );
// var THREEeffectBloom1 = new THREE.BloomPass( 1, 15, 2, 512 );
// var THREEeffectBloom2 = new THREE.BloomPass( 1, 25, 3, 512 );
// var THREEeffectBloom3 = new THREE.BloomPass( 1, 25, 4, 512 );
// THREEeffectBloom1.clear = true;
// THREEeffectBloom2.clear = true;
// THREEeffectBloom3.clear = true;
// THREEeffectCopy.renderToScreen = true;
// //
// var THREEpars = {
// generateMipmaps: true,
// minFilter: THREE.LinearMipmapLinearFilter,
// magFilter: THREE.LinearFilter,
// format: THREE.RGBFormat,
// stencilBuffer: false
// };
// var THREErtwidth = 512;
// var THREErtheight = 512;
//
// THREEcomposer = new THREE.EffectComposer( THREErenderer, new THREE.WebGLRenderTarget( THREErtwidth, THREErtheight, THREEpars ) );
THREEcomposer.addPass( THREErenderModelUV );
console.log(THREEcomposer)
}, 2000)
}, 2000)
}, 2000)
}, 2000)
},1000)
in other areas of the project, I wouldn't recommend looking at since it's really not relevant to this issue, the points that I highlighted are the only areas that deal with rendering, adding the mesh, and applying postprocessing.
the uniforms for the material are set up in the html file "index-shader.html" between lines 655 and 700 which may also be where I'd need to duplicate the uniforms and apply them properly, but I can't seem to figure out how to do that.
Please let me know if you have any help, thank you for reading!

How To Get Clicked Geometry’s Name Attribute From BufferGeometry in ThreeJS?

I’m developing a VueJs application and added 1000 box model to the scene. Although I’ve set the every geometry’s name attribute when I clicked the box model I couldn’t get the box geometry’s name. I think this is caused by applying merged buffer geometry. Is it possible to get name attribute by clicking in some way ?
Here is my code;
addCubeToScene() {
this.oDracoLoader = new DRACOLoader();
this.oDracoLoader.setDecoderPath("./draco/");
this.oGltfLoader = new GLTFLoader();
this.oGltfLoader.setDRACOLoader(this.oDracoLoader);
this.oDracoLoader.preload();
this.oGltfLoader.load(BoxModel, function(oObject) {
oObject.scene.traverse(function(oObject) {
if (oObject.isMesh) {
var oObjectGeometry = oObject.geometry;
for (var i = 0; i < 1000; i++) {
var oGeometry = oObjectGeometry.clone();
oGeometry.applyMatrix4(
new THREE.Matrix4().makeTranslation(
Math.random() * 3000,
Math.random() * 5000,
Math.random() * 1000
)
);
oGeometry.name = "Box-" + i;
oCubes.push(oGeometry);
}
var oGeometriesCubes = BufferGeometryUtils.mergeBufferGeometries(
oCubes
);
oGeometriesCubes.computeBoundingSphere();
const oTexture = new THREE.TextureLoader().load(BoxTexture);
oTexture.magFilter = THREE.NearestFilter;
var oMesh = new THREE.Mesh(
oGeometriesCubes,
new THREE.MeshLambertMaterial({
map: oTexture,
side: THREE.DoubleSide,
})
// new THREE.MeshNormalMaterial()
);
oMesh.scale.set(0.5, 0.5, 0.5);
oThis.scene.add(oMesh);
oThis.renderScene();
}
});
});
}
onMouseClick(oEvent) {
oEvent.preventDefault();
oEvent.stopPropagation();
oMouse.x =
(oEvent.offsetX / this.renderer.domElement.clientWidth) * 2 - 1;
oMouse.y =
-(oEvent.offsetY / this.renderer.domElement.clientHeight) * 2 + 1;
oRaycaster.setFromCamera(oMouse, this.camera);
var oIntersects = oRaycaster.intersectObjects(this.scene.children, true);
if (oIntersects[0] && oIntersects[0].object) {
var oObject = oIntersects[0].object;
console.log(oObject); // I would like to get clicked geometry's name from this object
}
}
Here is the image representing box model’s view;
Here is the result of box geometry datas
When I merged all geometries I can't see any name attribute
I assume you're trying to merge the geometry to increase render performance. While you did manage to reduce the number of draw calls the GPU had to make, you also removed all uniqueness of the different BufferGeometry objects. Your geometry is now represented by one big buffer (debug oGeometriesCubes.attributes.position.array to see this).
In the interest of performance, we can not only save you GPU draw calls, but also memory, and we can get your box picking to work as well. Let's start from scratch. It will seem like we've taken a step backward, but then it will all fall together...
Your geometry definitions
First, you do not need to uniquely identify each BufferGeometry. Mesh objects can share geometry! AND materials! This equates to memory savings.
Having unique Mesh objects also means you can put the name and translation information at the Mesh level. Translating at the geometry level 1000 times is expensive, even if you do it up-front like you do. Trust that the GPU can do these translations very quickly from the Mesh level. It's literally the GPU's job.
Your mesh definitions
Now, we're still looking at 1000 meshes, which means 1000 draw calls, right? That's where InstancedMesh comes into play.
Instancing--in very simple terms--is a way for the GPU to not only re-use the geometry buffer information, but also to perform the drawing of all 1000 meshes in a single swipe, rather than one at a time.
To sum it up:
Using only one BufferGeometry object...
And one material...
We'll draw 1000 instances...
In one draw call.
Ready? Let's do it.
The code
let geometry = yourGeometryFromGLTF;
// load your texture here...
let material = new new THREE.MeshLambertMaterial({
map: oTexture,
side: THREE.DoubleSide,
})
let iMesh = new THREE.InstancedMesh( geometry, material, 1000 );
let translateMatrix = new THREE.Matrix4();
let scaleMatrix = new THREE.Matrix4().makeScale( 0.5, 0.5, 0.5 );
let finalMatrix = new THREE.Matrix4();
for( let i = 0; i < 1000; ++i ){
​
​translateMatrix.makeTranslation(
​Math.random() * 1500,
​Math.random() * 2500,
​Math.random() * 500
​ );
finalMatrix.multiplyMatrices( translateMatrix, scaleMatrix );
​
iMesh.setMatrixAt( i, finalMatrix );
}
imesh.instanceMatrix.needsUpdate = true; // IMPORTANT
scene.add( iMesh );
Now your Raycaster should return the instances, but it will take one more step to get the "ID" of which box you selected.
let intersects = raycaster.intersectObjects(scene);
if(intersects.length > 0){
let boxName = `Box-${ intersects[0].instanceId }`;
}
The instanceId will be the index of the box instance in your InstancedMesh. You can even use it to get more information about that instance, like its transformation matrix:
let iMatrix = new THREE.Matrix4();
iMesh.getMatrixAt( intersects.instanceId, iMatrix );

Animate point with paper.js

I’d like to rebuild this animation http://imgur.com/l5Vhswe in paper.js.
I already tried SVG animations (http://codepen.io/magglomag/pen/jrVwzy) but despite from the fact that they’ll be deprecated soon I was not able to move the two points asynchronously.
What I have so far is the shape and I know that I can animate with the onFrame event handler. But I have no clue how to say that the point should animate between the coordinates [43,168.7] and [43,35.3].
http://codepen.io/magglomag/pen/yaVXrr
var firstSegment = new Segment({
point: [109,3.7]
});
var secondSegment = new Segment({
point: [43,168.7]
});
var thirdSegment = new Segment({
point: [109,202.2]
});
var path = new Path({
segments: [firstSegment, secondSegment, thirdSegment],
fillColor: '#2dfd9a',
closed: true
});
secondSegment.onFrame = function(event) {
this.point = [43,35.3]
}
The error you are making is that you are trying to bind an handler to segment.onFrame event.
But only item.onFrame and view.onFrame are available.
In PaperScript context, you can even use a global onframe named function as a convenient way to animate things.
Here is a simple example demonstrating how a path segment can be animated.
// create a triangle
var triangle = new Path.RegularPolygon({
center: view.center,
sides: 3,
radius: 50,
fillColor: 'orange'
});
// store initial first point position
var initialPoint = triangle.firstSegment.point.clone();
// on frame
function onFrame(event) {
// use sine function as a convenient way to demonstrate animation
var newPoint = initialPoint + Math.sin(event.count * 0.05) * 30;
// update first point
triangle.firstSegment.point = newPoint;
}

Three.js: Add a texture to an object just on the outside

I'm very new with Three.js and I'm trying to make a ring:
http://www.websuvius.it/atma/myring/preview.html
I have a background texture ( the silver one ) and another one with a text.
I want the text only on the ring external face.
This is part of my code:
var loader = new THREE.OBJLoader( manager );
var textureLoader = new THREE.TextureLoader( manager );
loader.load( 'assets/3d/ring.obj', function ( event ) {
var object = event;
var geometry = object.children[ 0 ].geometry;
var materials = [];
var backgroundTexture = textureLoader.load('img/texture/silver.jpg');
backgroundTexture.flipY = false;
var background = new THREE.MeshBasicMaterial({
map: backgroundTexture,
color: 0xffffff
});
materials.push(background);
var customTexture = textureLoader.load('img/text.png');
customTexture.flipY = false;
var custom = new THREE.MeshBasicMaterial({
map: customTexture,
transparent: true,
opacity: 1,
color: 0xffffff
});
materials.push(custom);
mesh = THREE.SceneUtils.createMultiMaterialObject(geometry, materials);
mesh.position.y=-50;
scene.add(mesh);
}, onProgress, onError );
It is possible?
Thanks
The reason behind your issue appears to be in your .obj file. Judging from a quick glance at the texture coordinates stored in the file, the inside of the ring uses the same part of the texture image as the outside of the ring.
Increasing the transparent parts of the image won't help. Neither will the attempts to stop the texture from repeating. Those would help if the texture coordinates were larger than 1 but this is not your case unfortunately.
However, there are several solutions:
Split the object in a 3D modeling software to two objects - outside and inside of the ring - and apply the texture only to the first one.
Adjust the UV coordinates of the object in a 3D modeling software.
Adjust the UV coordinates of the vertices programmatically after loading the object to Three.JS

Resources