Three.js Using 2D texture\sprite for animation (planeGeometry)

26,420

Solution 1

I had the same question a while ago, and so I have written up a complete example of animating using a spritesheet as the texture for a PlaneGeometry, and then updating the texture at regular intervals -- check out the example at

http://stemkoski.github.io/Three.js/Texture-Animation.html

and view the commented source code for additional explanation.


Update (2021):

Here is an updated version of the function I recommend using. It fixes the issue with the incorrect tile display order, it automatically updates the next frame, and it returns an object you can use to stop and re-start the animation as desired.

function TextureAnimator(texture, tilesHoriz, tilesVert, tileDispDuration) 
{ 
  let obj = {};

  obj.texture = texture;
  obj.tilesHorizontal = tilesHoriz;
  obj.tilesVertical = tilesVert;
  obj.tileDisplayDuration = tileDispDuration;

  obj.numberOfTiles = tilesHoriz * tilesVert;

  obj.texture.wrapS = THREE.RepeatWrapping;   
  obj.texture.wrapT = THREE.RepeatWrapping;   
  obj.texture.repeat.set( 1/tilesHoriz, 1/tilesVert );
  obj.currentTile = 0;

  obj.nextFrame = function()
  {
    obj.currentTile++;
    if (obj.currentTile == obj.numberOfTiles)
      obj.currentTile = 0;

    let currentColumn = obj.currentTile % obj.tilesHorizontal;
    obj.texture.offset.x = currentColumn / obj.tilesHorizontal;

    let currentRow = Math.floor( obj.currentTile / obj.tilesHorizontal );
    obj.texture.offset.y = obj.tilesVertical - currentRow / obj.tilesVertical;
  }

  obj.start = function()
  { obj.intervalID = setInterval(obj.nextFrame, obj.tileDisplayDuration); }

  obj.stop = function()
  { clearInterval(obj.intervalID); }

  obj.start();
  return obj;
}   

Solution 2

I've noted in my comment to Lee Stemkoski that spritesheets that have more than one row do not work the same when using the newer THREE.TextureLoader().

I am using the following 4x4 sprite image in my tests.

Image showing quadrant highlighted for each expected tile

With no modification to Lee Stemkoski's TextureAnimator function, assuming you have a full 16 tile spritesheet.

var texture = new THREE.TextureLoader().load('grid-sprite.jpg');
var annie = new TextureAnimator(texture, 4, 4, 16, 150);

The animated texture runs backwards. Codepen Demo

enter image description here

So I made my own which I call 🎉🎉🎉 THREE.SpriteSheetTexture 🎉🎉🎉

THREE.SpriteSheetTexture = function(imageURL, framesX, framesY, frameDelay, _endFrame) {

    var timer, frameWidth, frameHeight,
            x = 0, y = 0, count = 0, startFrame = 0, 
            endFrame = _endFrame || framesX * framesY,
            CORSProxy = 'https://cors-anywhere.herokuapp.com/',
            canvas = document.createElement('canvas'),
            ctx = canvas.getContext('2d'),
            canvasTexture = new THREE.CanvasTexture(canvas),
            img = new Image();

    img.crossOrigin = "Anonymous"
    img.onload = function(){
        canvas.width = frameWidth = img.width / framesX;
        canvas.height = frameHeight = img.height / framesY;
        timer = setInterval(nextFrame, frameDelay);
    }
    img.src = CORSProxy + imageURL;

    function nextFrame() {
        count++;

        if(count >= endFrame ) {
            count = 0;
        };

        x = (count % framesX) * frameWidth;
        y = ((count / framesX)|0) * frameHeight;

        ctx.clearRect(0, 0, frameWidth, frameHeight);
        ctx.drawImage(img, x, y, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight);

        canvasTexture.needsUpdate = true;
    }

    return canvasTexture;
}

And what you need to know about it imageURL is the URL of your spritesheet framesX is how many frames fit along the x axis (left and right) framesY is how many frames fit along the y axis (up and down) delay is how long it the texture waits to change to the next frame _endFrame is optional - How many frames are there (in case it doesnt use a full row)

That all looks something like this

texture = new THREE.SpriteSheetTexture('https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/grid-sprite.jpg', 4, 4, 100, 16);
var material = new THREE.MeshBasicMaterial({ 
    map: texture
});
geometry = new THREE.BoxGeometry( 200, 200, 200 );
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

And there was much rejoicing!!! Codepen Demo Here

Solution 3

To make frames move in the right direction use:

texture.offset.y = (1 - currentRow / _tilesVertical) - (1 / _tilesVertical);

instead of

texture.offset.y = currentRow / this.tilesVertical;

Solution 4

@Cmndo to make frames flow moves in the right order you just need to update this:

texture.offset.y = currentRow / this.tilesVertical;

to this:

texture.offset.y = this.tilesVertical - (currentRow / this.tilesVertical);

In this example: https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/Texture-Animation.html

Share:
26,420
Spolarium7
Author by

Spolarium7

Updated on November 01, 2021

Comments

  • Spolarium7
    Spolarium7 over 2 years

    I'm quite new in html5 and three.js. I've been experimenting a bit with it, and basically what I want done is to have a Mesh (I'm using planeGeometry, as the tutorial I followed used it). The Mesh shows different Textures, which can change later on.

    Here's what my code looks like:

    angelTexture = THREE.ImageUtils.loadTexture("images/textures/chars/angel/angel.png");
    angelTexture.offset.x = -0.75;
    angelTexture.offset.y = -0.75;
    
    angelMesh = new THREE.Mesh( new THREE.PlaneGeometry(79, 53, 79, 53), new THREE.MeshBasicMaterial( { map: angelTexture, wireframe: false } ));
    
    angelMesh.position.x = 0;
    angelMesh.position.y = 0;
    scene.add(angelMesh);
    

    The problem is that whenever I offset, the Mesh seems big enough to show all the other Sprites (I'm using the texture as a 2D Sprite that I offset to animate it). The result is quite disastrous and I am still figuring out how to control how big the Mesh is so that it shows only one snapshot of the Sprite. All my attempts seem only to resize the Mesh as well as the underlying Texture and still shows all the Sprites.

    Can someone point me in the right direction? Thanks in advance.

    ...

    My friend came up with a solution... I missed the repeat property.

    angelTexture = THREE.ImageUtils.loadTexture("images/textures/chars/angel/angel.png");
    angelTexture.offset.x = -0.75; 
    angelTexture.offset.y = -0.75;
    
    angelTexture.repeat.x = 0.25;
    angelTexture.repeat.y = 0.25;   
    scene.add(angelMesh);
    

    Hope this helps others having the same problem.

  • Motionharvest
    Motionharvest over 5 years
    This code has a flaw. I don't know when the flaw began, but the texture is rendered to the bottom left corner of the plane, and the code that is supposed to let you control the Y offset does not do its job. I'm working to understand the problem better and will respond once I figure it out.
  • Inkh Su Tesou
    Inkh Su Tesou over 4 years
    Not quickly able to use this as a THREE.Sprite, your demo only includes a THREE.Box
  • Motionharvest
    Motionharvest over 4 years
    Works here just fine. Here's a demo codepen.io/motionharvest/pen/…
  • MlleBz
    MlleBz over 2 years
    Hi Lee ! Thanks for your function, but tileDispDuration doesn't work with this update. Can you correct it ? I can't find the way. Thanks !
  • richy
    richy about 2 years
    This doesn't quite work when you have less frames than spaces, try m4chei's answer