Threejs FPS with text objects - animation

I'm working on a ThreeJS project where there is a text tunnel, created by generating text objects with TextGeometry, and moving them along the z axis continuously. The objects are generated dynamically. I use 30 at a time with a circular buffer.
For some reason, I get a good FPS (around 60 or more), but it varies a lot. There are drops in FPS every 5-10 seconds or so. I've tested the code in several conditions (i.e. not using other elements like other objects, video capture, cam movement, etc.) and I am pretty sure the moving text objects are the cause of drop in FPS.
Ideas?
ThreeJS update function:
function update()
{
bookidx = bookidx % book.length;
// FLOATING TEXT
if (stepsFloating++ % floatingWordsRate == 0 && makeFloatingWords){
addFloatingText(book[bookidx], scene);
}
else{
floatingWords.move();
}
// TUNNEL TEXT
// move sentences forward
for (t in tunnelWords.tokens){
tunnelWords.tokens[t][1].position.z += 120;
}
// get another sentence
if (tunnelWords.tokens[0][1].position.z > -2000){
addTunnelText(book[bookidx++], scene);
}
// KEYBOARD
keyboardUpdate();
// CONTROLS AND STATS
controls.update();
stats.update();
}
This creates the object (I intentionally use a high bevel thickness for the effect, decreasing it or not using it at all did not make a difference:
function makeTextMeshForTunnel(word){
var textGeom = new THREE.TextGeometry( word ,
{
size: 22, height: 1, curveSegments: 1,
font: "helvetiker", weight: "normal", style: "normal",
bevelThickness: 900, bevelSize: 2, bevelEnabled: true,
material: 1, extrudeMaterial: 0
});
// font: helvetiker, gentilis, droid sans, droid serif, optimer
// weight: normal, bold
var textMesh = new THREE.Mesh(textGeom, tunnel_textMaterial );
textGeom.computeBoundingBox();
var textWidth = textGeom.boundingBox.max.x - textGeom.boundingBox.min.x;
textMesh.position.set( -0.5 * textWidth, 0, -3000 );
textMesh.rotation.x = TUNNEL_TEXT_X_ROTATION;
return textMesh;
}

This helps, seems textGeometry is more costly than other geometries, this helps to understand the problem with suggested solutions:
https://github.com/mrdoob/three.js/issues/1825

Related

How can an AnimationAction stop at the last frame without looping in three.js?

I'd like to stop an AnimationAction at the last frame that I've created with morph targets.
https://threejs.org/docs/#api/en/animation/AnimationAction
I've tried animationAction.clampWhenFinished = true; but that doesn't seem to work.
I've looked at older stackoverflow questions and searched through forums but the solutions didn't work.
var cubeTarget1 = new THREE.BoxGeometry(20, 10, 10);
var cubeTarget2 = new THREE.BoxGeometry(20, 10, 50);
var cubeTarget3 = new THREE.BoxGeometry(60, 10, 10);
cubeGeometry.morphTargets[0] = {name: 't1', vertices: cubeTarget1.vertices};
cubeGeometry.morphTargets[1] = {name: 't2', vertices: cubeTarget2.vertices};
cubeGeometry.morphTargets[2] = {name: 't3', vertices: cubeTarget3.vertices};
Is there a way I can do something like: (this doesn't work, it loops back to the first morphTarget)
var clip1 = THREE.AnimationClip.CreateFromMorphTargetSequence('run', [cubeGeometry.morphTargets[0],cubeGeometry.morphTargets[1]], 30);
var action1 = mixer.clipAction(clip1);
action1.play(); // starts at cubeTarget1 ends at cubeTarget2 (animating between them, without a loop)
// and at a later point I'd like to do
var clip2 = THREE.AnimationClip.CreateFromMorphTargetSequence('run', [cubeGeometry.morphTargets[1],cubeGeometry.morphTargets[2]], 30);
var action2 = mixer.clipAction(clip2);
action2.play(); // starts at cubeTarget2 ends at cubeTarget3 (animating between them, without a loop)
Here's my fiddle:
https://jsfiddle.net/foreyez/uy8abk6v/
This is my approach that I used with an enemy bot gltf model on my three.js prototype first person shooter. The robot has a single track animation with many frames. I had to split the frames up into sub clips with the following code then applied clampwhenfinished.
var EnemyHeavyBotFallBackClip = THREE.AnimationUtils.subclip(gltf.animations[0], “Take_001”, 1300, 1355);
actionEnemyHeavyBotFallBackMixer = mixer.clipAction(EnemyHeavyBotFallBackClip);
actionEnemyHeavyBotFallBackMixer.clampWhenFinished = true;
actionEnemyHeavyBotFallBackMixer.setLoop(THREE.LoopOnce);
actionEnemyHeavyBotFallBackMixer.play();
https://www.shanebrumback.com/super-soldier-battle-intro.html
Disclaimer: This is my website.
I looked at the three.js code. And inside LoopOnce the section involving clampWhenFinished doesn't get hit at all.
For now I'll do it in a very crude way until I find a better solution:
action.setDuration(5).play();
setTimeout(function()
{
action.paused = true;
},2500); // half of the duration
Another way I've been doing is to use morphTargetInfluences and just increment it on an animation loop:
function animate() {
if (cube.morphTargetInfluences[0] < 1)
cube.morphTargetInfluences[0] += 0.01;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Use Tween.js if you need more functionality.
It took me a while to get this working, a lot of the online examples seem to be outdated and/or non-working. Try:
var clips = THREE.AnimationClip.CreateClipsFromMorphTargetSequences(geometry.morphAttributes.position, 60, true);
mixer = new THREE.AnimationMixer(points);
var action = mixer.clipAction(clips[0]).setDuration(10);
action.clampWhenFinished = true;
action.setLoop(THREE.LoopOnce);
action.play();
Note the "noLoop" parameter of CreateClipsFromMorphTargetSequences(name: String, morphTargetSequence: Array, fps: Number, noLoop: Boolean) needs to be "true" along with clampWhenFinished = true and setLoop(THREE.LoopOnce).
Full example here https://jsfiddle.net/jm4ersoq/

Enable/disable clipping per material (r87)

I'm trying to work with THREE's clipping planes, I didn't read the description of Material.clipIntersection and just blindly took it to mean "is clipping enabled".
After reading the description, playing with the example and digging through code I've concluded that there is no parameter to control wether the clipping is enabled or not. The only two interfaces are:
.clippingPlanes[]
.clipIntersection
And perhaps Renderer.localClippingEnabled but i don't want to globally enable/disable the... local clipping. Ie. if i have two materials, i'd like to be able to control it on one.
The problem seems to be that clippingPlanes defines NUM_CLIPPING_PLANES:
'#define NUM_CLIPPING_PLANES ' + parameters.numClippingPlanes,
And I can see that there is more stuff going on with WebGLClipping. Still i'm confused by the define and am wondering if i need to update the material every time i add/remove the clipping planes.
tl:dr;
Is there a built in way to easily add a toggle to enable/disable the clipping to this example:
https://threejs.org/examples/#webgl_clipping_intersection, without recompiling the shader?
Probably not in the spirit of your question, but the easy solution is to replace the clippingPlanes property with an empty array.
I changed/added the following code in the example:
var params = {
clipPlanesOn: true,
clipIntersection: true,
planeConstant: 0,
showHelpers: false
};
...
var clipPlanes = [
new THREE.Plane( new THREE.Vector3( 1, 0, 0 ), 0 ),
new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 0 ),
new THREE.Plane( new THREE.Vector3( 0, 0, - 1 ), 0 )
];
var clipPlanesOff = [];
...
gui.add( params, 'clipPlanesOn' ).name( 'clip planes on' ).onChange( function ( value ) {
var children = group.children;
for ( var i = 0; i < children.length; i ++ ) {
children[ i ].material.clippingPlanes = (value) ? clipPlanes : clipPlanesOff;
}
render();
} );
Checking the box turns the clipping on/off.
Update with more info:
AFAICT, it's all "automagic". Each render during setProgram, WebGLRenderer sets a variable called _clippingEnabled. This is set based on the return value from WebGLClipping.init, where one of the cases checks if the material has any values in its clippingPlanes property.
WebGLClipping has the following line:
uniform = { value: null, needsUpdate: false };
With _clippingEnabled set to true, and some values in the clippingPlanes property, the process makes its way into projectPlanes of WebGLClipping, which includes the line:
uniform.needsUpdate = true;
Boom. The uniform is flagged for automagic update.

Accelerated canvas context translation: bug, or my mistake?

I found an odd behavior while working on my pet game. I wanted to draw few objects on canvas, some of them required image / icon to be rotated. It is quite common use case, usual solution is to make use of context's rotate method. Going with the blow I used also translate to nicely and consistently put images in desired place.
This worked fine, until I tried Chrome on Windows laptop, with hardware acceleration enabled. Images started to blink and teleport across the screen. I found that it is related to acceleration (turning off accelerated graphics fixes problem) and my best guess is that when updating frame the renderer assumes that drawing calls are independent and can be executed in parallel. When context transforms take place it is not the case because context state changes.
Example of undesired behavior: having a canvas element with ID 'screen' try the following:
var canvas = document.getElementById("screen"),
ctx = canvas.getContext("2d");
var drawrect = function () {
ctx.fillStyle = this.color;
ctx.translate(this.x+10, this.y+10);
ctx.rotate(this.rotation);
ctx.fillRect(-10, -10, 20, 20);
ctx.rotate(-this.rotation);
ctx.translate(-this.x-10, -this.y-10);
};
var red = {
x: 22,
y: 22,
rotation: 0,
color: "#ff0000",
draw: drawrect
};
var blu = {
x: 22,
y: 111,
rotation: 0,
color: "#0000ff",
draw: drawrect
};
function main_loop() {
ctx.clearRect(0, 0, 450, 450);
frameId = requestAnimationFrame(main_loop);
red.draw();
red.x += 1;
red.rotation +=0.1;
blu.draw();
blu.x += 1;
blu.rotation -=0.1;
}
main_loop();
Working example: http://jsfiddle.net/1u2d7uhr/7/ (tested on Chrome, Chromium, Firefox; accelerated Chrome glitches, others do not)
I was able to 'fix' this by removing translations and rendering rotating elements to separate canvas, which is then (after rotations) drawn onto the main one. This seems hackish to me though.
Is it code error on my part?
In this case what is the right way to render rotations (perhaps with this question I should go do codereview, but I'm not sure if this is the case)?
Or is it buggy behavior on browser side? I understand the logic behind it but it can be very much surprising (and cause some confusion) to developers. Or am I only one...

Using CreateJS to apply an alpha tween to a drawn line

I'm trying to take a users mouse/touch drawn line and then have it alpha fade out the result using a tween. The problem is when cap and joint style are set to rounded then joint point fades behind the rest of the line. It looks fine when set to miter or bevel.
What I want is a smooth solid fade of the shape. Any ideas?
Fiddle: http://jsfiddle.net/mcfarljw/ZNGK2/
Function for drawing the line based on user input:
function handleMouseMove(event) {
var midPt = new createjs.Point(oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);
drawingCanvas.graphics.setStrokeStyle(stroke, 'round', 'round').beginStroke(color).moveTo(midPt.x, midPt.y).curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);
oldPt.x = stage.mouseX;
oldPt.y = stage.mouseY;
oldMidPt.x = midPt.x;
oldMidPt.y = midPt.y;
stage.update();
}
Tween applied to the shape after line is finished:
createjs.Tween.get(drawingCanvas).to({
alpha: 0
}, 2000).call(function() {
drawingCanvas.alpha = 1;
drawingCanvas.graphics.clear();
});
You'll want to cache the whole shape before fading it out. See the updates I have made to the fiddle. Mainly, take a look at line 52 on the handleMouseUp event.
drawingCanvas.cache(0, 0, 800, 800);
Then, when your fade is complete. Make sure to uncache before showing the object again. Otherwise your graphics.clear() won't work.
drawingCanvas.uncache();

Multiple clipping areas on Fabric.js canvas

For making Photo Collage Maker, I use fabric js which has an object-based clipping feature. This feature is great but the image inside that clipping region cannot be scaled, moved or rotated. I want a fixed position clipping region and the image can be positioned inside the fixed clipping area as the user want.
I googled and find very near solution.
var canvas = new fabric.Canvas('c');
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(10,10,150,150);
ctx.rect(180,10,200,200);
ctx.closePath();
ctx.stroke();
ctx.clip();
Multiple Clipping Areas on fabric js canvas
where the image of one clipping region has appeared in another clipping region. How can I avoid this or is there another way of accomplishing this using fabric js.
This can be accomplished with Fabric using the clipTo property, but you have to 'reverse' the transformations (scale and rotation), in the clipTo function.
When you use the clipTo property in Fabric, the scaling and rotation are applied after the clipping, which means that the clipping is scaled and rotated with the image. You have to counter this by applying the exact reverse of the transformations in the clipTo property function.
My solution involves having a Fabric.Rect serve as the 'placeholder' for the clip region (this has advantages because you can use Fabric to move the object around and thus the clip region.
Please note that my solution uses the Lo-Dash utility library, particularly for _.bind() (see code for context).
Example Fiddle
Breakdown
1. Initialize Fabric
First, we want our canvas, of course:
var canvas = new fabric.Canvas('c');
2. Clip Region
var clipRect1 = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 180,
top: 10,
width: 200,
height: 200,
fill: 'none',
stroke: 'black',
strokeWidth: 2,
selectable: false
});
We give these Rect objects a name property, clipFor, so the clipTo functions can find the one by which they want to be clipped:
clipRect1.set({
clipFor: 'pug'
});
canvas.add(clipRect1);
There doesn't have to be an actual object for the clip region, but it makes it easier to manage, as you're able to move it around using Fabric.
3. Clipping Function
We define the function which will be used by the images' clipTo properties separately to avoid code duplication:
Since the angle property of the Image object is stored in degrees, we'll use this to convert it to radians.
function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
findByClipName() is a convenience function, which is using Lo-Dash, to find the with the clipFor property for the Image object to be clipped (for example, in the image below, name will be 'pug'):
function findByClipName(name) {
return _(canvas.getObjects()).where({
clipFor: name
}).first()
}
And this is the part that does the work:
var clipByName = function (ctx) {
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
ctx.translate(0,0);
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.left,
clipRect.top - this.top,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
NOTE: See below for an explanation of the use of this in the function above.
4. fabric.Image object using clipByName()
Finally, the image can be instantiated and made to use the clipByName function like this:
var pugImg = new Image();
pugImg.onload = function (img) {
var pug = new fabric.Image(pugImg, {
angle: 45,
width: 500,
height: 500,
left: 230,
top: 170,
scaleX: 0.3,
scaleY: 0.3,
clipName: 'pug',
clipTo: function(ctx) {
return _.bind(clipByName, pug)(ctx)
}
});
canvas.add(pug);
};
pugImg.src = 'https://fabricjs.com/lib/pug.jpg';
What does _.bind() do?
Note that the reference is wrapped in the _.bind() function.
I'm using _.bind() for the following two reasons:
We need to pass a reference Image object to clipByName()
The clipTo property is passed the canvas context, not the object.
Basically, _.bind() lets you create a version of the function that uses the object you specify as the this context.
Sources
https://lodash.com/docs#bind
https://fabricjs.com/docs/fabric.Object.html#clipTo
https://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/
I have tweaked the solution by #natchiketa as the positioning of the clip region was not positioning correctly and was all wonky upon rotation. But all seems to be good now. Check out this modified fiddle:
https://jsfiddle.net/PromInc/ZxYCP/
The only real changes were made in the clibByName function of step 3 of the code provided by #natchiketa. This is the updated function:
var clipByName = function (ctx) {
this.setCoords();
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
var ctxLeft = -( this.width / 2 ) + clipRect.strokeWidth;
var ctxTop = -( this.height / 2 ) + clipRect.strokeWidth;
var ctxWidth = clipRect.width - clipRect.strokeWidth + 1;
var ctxHeight = clipRect.height - clipRect.strokeWidth + 1;
ctx.translate( ctxLeft, ctxTop );
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.oCoords.tl.x,
clipRect.top - this.oCoords.tl.y,
ctxWidth,
ctxHeight
);
ctx.closePath();
ctx.restore();
}
Two minor catches I found:
Adding a stroke to the clipping object seems to throw things off by a few pixels. I tried to compensate for the positioning, but then upon rotation, it would add 2 pixels to the bottom and right sides. So, I've opted to just remove it completely.
Once in a while when you rotate the image, it will end up with a 1px spacing on random sides in the clipping.
Update to #Promlnc answer.
You need to replace the order of context transformations in order to perform proper clipping.
translation
scaling
rotation
Otherwise, you will get wrongly clipped object - when you scale without keeping aspect ratio (changing only one dimension).
Code (69-72):
ctx.translate( ctxLeft, ctxTop );
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
Replace to:
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.rotate(degToRad(this.angle * -1));
See this:
https://jsfiddle.net/ZxYCP/185/
Proper result:
UPDATE 1:
I have developed a feature to clip by polygon:
https://jsfiddle.net/ZxYCP/198/
This can be done much more easily. Fabric provides render method to clip by the context of another object.
Checkout this fiddle. I saw this on a comment here.
obj.clipTo = function(ctx) {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
clippingRect.render(ctx);
ctx.restore();
};
As I tested all fiddles above they have one bug. It is when you will flip X and Y values together, clipping boundaries will be wrong. Also, in order not doing all calculations for placing images into the right position, you need to specify originX='center' and originY='center' for them.
Here is a clipping function update to original code from #natchiketa
var clipByName = function (ctx) {
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
ctx.translate(0,0);
//logic for correct scaling
if (this.getFlipY() && !this.getFlipX()){
ctx.scale(scaleXTo1, -scaleYTo1);
} else if (this.getFlipX() && !this.getFlipY()){
ctx.scale(-scaleXTo1, scaleYTo1);
} else if (this.getFlipX() && this.getFlipY()){
ctx.scale(-scaleXTo1, -scaleYTo1);
} else {
ctx.scale(scaleXTo1, scaleYTo1);
}
//IMPORTANT!!! do rotation after scaling
ctx.rotate(degToRad(this.angle * -1));
ctx.beginPath();
ctx.rect(
clipRect.left - this.left,
clipRect.top - this.top,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
Please check the updated fiddle
With the latest update on fabric 1.6.0-rc.1, you are able to skew the image by hold shift and drag the middle axis.
I have trouble with how to reverse the skew so that the clipping area stays the same. I have tried the following code to try to reverse it back, but didn't work.
var skewXReverse = - this.skewX;
var skewYReverse = - this.skewY;
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.transform(1, skewXReverse, skewYReverse, 1, 0, 0);
ctx.rotate(degToRad(this.angle * -1));
Demo: https://jsfiddle.net/uimos/bntepzLL/5/
Update to previous guys answers.
ctx.rect(
clipRect.oCoords.tl.x - this.oCoords.tl.x - clipRect.strokeWidth,
clipRect.oCoords.tl.y - this.oCoords.tl.y - clipRect.strokeWidth,
clipRect.oCoords.tr.x - clipRect.oCoords.tl.x,
clipRect.oCoords.bl.y - clipRect.oCoords.tl.y
);
Now we are able to scale the clipping area without a doubt.

Resources