使用three.js将精灵(纹理图集)拆分成瓷砖

时间:2017-12-01 12:39:34

标签: three.js imagemagick

目标是从给定的一组图片创建许多缩略图,假设所有缩略图都具有相同的尺寸。

https://threejs.org/examples/#misc_ubiquity_test2是展示texture.wrapS / texture.wrapT + texture.offset方法的一个很好的示例,但它意味着克隆每个缩略图的纹理,这会影响性能。一个问题是:如何重用单个纹理?

考虑到16384x16384px限制,另一个需要考虑的方面是:如何准备多个精灵,加载相应的纹理并在瓷砖之间分配(缩略图)?

1 个答案:

答案 0 :(得分:2)

准备精灵

假设:

  • 原始资产具有相同的宽高比,例如~1.4852894x1949px
  • 我们即将在最后呈现128缩略图
  • imagemagic已安装
  • ./assets/images/thumbnails/是操作的工作目录
  • ./assets/images/sprite-0.jpg,...,./assets/images/sprite-<n>.jpg将是实际的精灵 - 水平地图集(单行瓷砖)

首先,让我们定义所需的缩略图尺寸。由于three.js要求每个纹理维度都是2的幂,因此可以将高度设置为256px,使宽度等于380px。这意味着每个精灵43个图块(43*380=16340,其中16384是总宽度的限制。)

构建一个精灵

  1. 清理./assets/images/thumbnails/original-selected/并复制43原始资产的一部分。

  2. 执行下面列出的一组步骤

  3. 将结果sprite.jpg重命名为sprite-<iteration>.jpg

  4. 步骤

    生成小资产:

    $ mogrify -path ./assets/images/thumbnails/small/ -resize 380x256 ./assets/images/thumbnails/original-selected/*.png
    

    用小资产建立精灵:

    $ convert +append ./assets/images/thumbnails/small/*.png ./assets/images/sprite.png
    

    请注意,精灵现在为16340x256,因此必须将其调整为16384x256 这两个维度都是2的幂(否则three.js将会动态执行此操作):

    $ convert -resize 16384x256\! ./assets/images/sprite.png ./assets/images/sprite.png
    

    最后,将精灵转换为JPEG,缩小尺寸:

    $ convert -quality 85 ./assets/images/sprite.png ./assets/images/sprite.jpg
    

    加载纹理和创建缩略图

    平铺自身(设置geometry.faceVertexUvs值)的灵感来自https://solutiondesign.com/blog/-/sdg/webgl-and-three-js-texture-mappi-1/19147

    import {Scene, Texture, TextureLoader, Vector2, PlaneGeometry, BufferGeometry, MeshBasicMaterial, Mesh} from 'three';
    
    const thumbnailWidth = 380;
    const thumbnailHeight = 256;
    const thumbnailsCount = 128;
    const spriteLength = 43;
    const spriteUrlPattern = 'assets/images/sprite-<index>.jpg';
    const scene = new Scene();
    const loader = new TextureLoader();
    
    loadAllTextures()
      .then(initializeAllThumbnails);
    
    function loadAllTextures(): Promise<Texture[]> {
      const spritesCount = Math.ceil(thumbnailsCount / spriteLength);
      const singlePromises = [];
    
      for (let i = 0; i < spritesCount; i += 1) {
        singlePromises.push(loadSingleTexture(i));
      }
    
      return Promise.all(singlePromises);
    }
    
    function loadSingleTexture(index: number): Promise<Texture> {
      const url = spriteUrlPattern.replace('<index>', String(index));
    
      return new Promise((resolve) => {
        loader.load(url, resolve);
      });
    }
    
    // Tiles are taken from different sprites,
    // so thumbnail meshes are built using corresponding textures.
    // E.g. given 128 tiles packed into 3 sprites,
    // thumbnails 0..43 take the 1st texture, 44..86 - the 2nd one and so on.
    function initializeAllThumbnails(allTextures: Texture[]) {
      const baseGeometry = new PlaneGeometry(thumbnailWidth, thumbnailHeight);
      const materials = allTextures.map((texture) => new MeshBasicMaterial({
        map: texture,
      }));
    
      for (let thumbnailIndex = 0; thumbnailIndex < thumbnailsCount; thumbnailIndex += 1) {
        const geometry = getThumbnailGeometry(thumbnailIndex, baseGeometry);
        const materialIndex = Math.floor(thumbnailIndex / spriteLength);
        const material = materials[materialIndex]; // could be cloned in here, if each material will need individual transformations, e.g. opacity
        const mesh = new Mesh(geometry, material);
    
        scene.add(mesh);
      }
    }
    
    function getThumbnailGeometry(thumbnailIndex: number, baseGeometry: PlaneGeometry): BufferGeometry {
      const tileWidth = 1 / spriteLength;
      const tileIndex = thumbnailIndex % spriteLength;
      const offset = tileIndex * tileWidth;
    
      // +---+---+---+
      // | 3 | . | 2 |
      // +---+---/---+
      // | . | / | . |
      // +---/---+---+
      // | 0 | . | 1 |
      // +---+---+---+
      const tile = [
        new Vector2(offset, 0),
        new Vector2(offset + tileWidth, 0),
        new Vector2(offset + tileWidth, 1),
        new Vector2(offset, 1),
      ];
    
      const plainGeometry = baseGeometry.clone();
      const bufferGeometry = new BufferGeometry();
    
      // a face consists of 2 triangles, coords defined counterclockwise
      plainGeometry.faceVertexUvs[0] = [
        [tile[3], tile[0], tile[2]],
        [tile[0], tile[1], tile[2]],
      ];
    
      bufferGeometry.fromGeometry(plainGeometry);
    
      return bufferGeometry;
    }