javascript - Three.js:逐步增加纹理分辨率的策略

标签 javascript three.js raster

我正在制作 Three.js 图表,它基本上表示二维平面中的一堆图像。

现在,单个图像是更大的 2048px x 2048px 图像图集文件的每个 32px x 32px 片段。当用户放大到场景的特定区域时,我想增加这些单个图像的大小。例如,如果用户开始放大空间最右侧区域的图像,我计划将该区域中的 32px x 32px 单个图像更新为内容相同的 64px x 64px 图像(以显示更多细节)。

我的问题是:Three.js 实现这个目标的方法是什么?

我措手不及的计划是加载更高分辨率的 Assets ,将它们映射到适当的几何坐标,然后简单地删除具有 32 像素子图像的旧网格并添加具有 64 像素子图像的新网格。我最初以为我可以只更新现存几何体的纹理/ Material ,但我读到不应使用大于 2048px x 2048px 的纹理,并且具有 n 个点的几何体不允许我不断提高保真度在不超过最大纹理尺寸的情况下使用该几何图形中的图像。

如果 Three.js 老手能就他们将如何完成这项任务提供任何见解,我将不胜感激!

完整代码:

/**
* Globals
**/

// Identify data endpoint
var dataUrl = 'https://s3.amazonaws.com/duhaime/blog/tsne-webgl/data/';

// Create global stores for image and atlas sizes
var image, atlas;

// Create a store for image position information
var imagePositions = null;

// Create a store for the load progress. Data structure:
// {atlas0: percentLoaded, atlas1: percentLoaded}
var loadProgress = {};

// Create a store for the image atlas materials. Data structure:
// {subImageSize: {atlas0: material, atlas1: material}}
var materials = {32: {}, 64: {}};

// Create a store for meshes
var meshes = [];

/**
* Create Scene
**/

// Create the scene and a camera to view it
var scene = new THREE.Scene();

/**
* Camera
**/

// Specify the portion of the scene visiable at any time (in degrees)
var fieldOfView = 75;

// Specify the camera's aspect ratio
var aspectRatio = window.innerWidth / window.innerHeight;

/*
Specify the near and far clipping planes. Only objects
between those planes will be rendered in the scene
(these values help control the number of items rendered
at any given time)
*/
var nearPlane = 100;
var farPlane = 50000;

// Use the values specified above to create a camera
var camera = new THREE.PerspectiveCamera(
  fieldOfView, aspectRatio, nearPlane, farPlane
);

// Finally, set the camera's position
camera.position.z = 12000;
camera.position.y = -2000;

/**
* Lights
**/

// Add a point light with #fff color, .7 intensity, and 0 distance
var light = new THREE.PointLight( 0xffffff, 1, 0 );

// Specify the light's position
light.position.set( 1, 1, 100 );

// Add the light to the scene
scene.add(light)

/**
* Renderer
**/

// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({ antialias: true });

// Add support for retina displays
renderer.setPixelRatio( window.devicePixelRatio );

// Specify the size of the canvas
renderer.setSize( window.innerWidth, window.innerHeight );

// Add the canvas to the DOM
document.body.appendChild( renderer.domElement );

/**
* Load External Data
**/

// Load the image position JSON file
var fileLoader = new THREE.FileLoader();
var url = dataUrl + 'image_tsne_projections.json';
fileLoader.load(url, function(data) {
  imagePositions = JSON.parse(data);
  conditionallyBuildGeometries(32)
})

/**
* Load Atlas Textures
**/

// List of all textures to be loaded, the size of subimages
// in each, and the total count of atlas files for each size
var textureSets = {
  32: { size: 32, count: 5 },
  64: { size: 64, count: 20 }
}

// Create a texture loader so we can load our image files
var textureLoader = new AjaxTextureLoader();

function loadTextures(size, onProgress) {
  setImageAndAtlasSize(size)
  for (var i=0; i<textureSets[size].count; i++) {
    var url = dataUrl + 'atlas_files/' + size + 'px/atlas-' + i + '.jpg';
    if (onProgress) {
      textureLoader.load(url,
        handleTexture.bind(null, size, i),
        onProgress.bind(null, size, i));
    } else {
      textureLoader.load(url, handleTexture.bind(null, size, i));
    }
  }
}

function handleProgress(size, idx, xhr) {
  loadProgress[idx] = xhr.loaded / xhr.total;
  var sum = 0;
  Object.keys(loadProgress).forEach(function(k) { sum += loadProgress[k]; })
  var progress = sum/textureSets[size].count;
  var loader = document.querySelector('#loader');
  progress < 1
    ? loader.innerHTML = parseInt(progress * 100) + '%'
    : loader.style.display = 'none';
}

// Create a material from the new texture and call
// the geometry builder if all textures have loaded 
function handleTexture(size, idx, texture) {
  var material = new THREE.MeshBasicMaterial({ map: texture });
  materials[size][idx] = material;
  conditionallyBuildGeometries(size, idx)
}

// If the textures and the mapping from image idx to positional information
// are all loaded, create the geometries
function conditionallyBuildGeometries(size, idx) {
  if (size === 32) {
    var nLoaded = Object.keys(materials[size]).length;
    var nRequired = textureSets[size].count;
    if (nLoaded === nRequired && imagePositions) {  
      // Add the low-res textures and load the high-res textures
      buildGeometry(size);
      loadTextures(64)
    }
  } else {
    // Add the new high-res texture to the scene
    updateMesh(size, idx)
  }
}

loadTextures(32, handleProgress)

/**
* Build Image Geometry
**/

// Iterate over the textures in the current texture set
// and for each, add a new mesh to the scene
function buildGeometry(size) {
  for (var i=0; i<textureSets[size].count; i++) {
    // Create one new geometry per set of 1024 images
    var geometry = new THREE.Geometry();
    geometry.faceVertexUvs[0] = [];
    for (var j=0; j<atlas.cols*atlas.rows; j++) {
      var coords = getCoords(i, j);
      geometry = updateVertices(geometry, coords);
      geometry = updateFaces(geometry);
      geometry = updateFaceVertexUvs(geometry, j);
      if ((j+1)%1024 === 0) {
        var idx = (i*textureSets[size].count) + j;
        buildMesh(geometry, materials[size][i], idx);
        var geometry = new THREE.Geometry();
      }
    }
  }
}

// Get the x, y, z coords for the subimage at index position j
// of atlas in index position i
function getCoords(i, j) {
  var idx = (i * atlas.rows * atlas.cols) + j;
  var coords = imagePositions[idx];
  coords.x *= 2200;
  coords.y *= 1200;
  coords.z = (-200 + j/10);
  return coords;
}

// Add one vertex for each corner of the image, using the 
// following order: lower left, lower right, upper right, upper left
function updateVertices(geometry, coords) {
  // Retrieve the x, y, z coords for this subimage
  geometry.vertices.push(
    new THREE.Vector3(
      coords.x,
      coords.y,
      coords.z
    ),
    new THREE.Vector3(
      coords.x + image.shownWidth,
      coords.y,
      coords.z
    ),
    new THREE.Vector3(
      coords.x + image.shownWidth,
      coords.y + image.shownHeight,
      coords.z
    ),
    new THREE.Vector3(
      coords.x,
      coords.y + image.shownHeight,
      coords.z
    )
  );
  return geometry;
}

// Create two new faces for a given subimage, then add those
// faces to the geometry
function updateFaces(geometry) {
  // Add the first face (the lower-right triangle)
  var faceOne = new THREE.Face3(
    geometry.vertices.length-4,
    geometry.vertices.length-3,
    geometry.vertices.length-2
  )
  // Add the second face (the upper-left triangle)
  var faceTwo = new THREE.Face3(
    geometry.vertices.length-4,
    geometry.vertices.length-2,
    geometry.vertices.length-1
  )
  // Add those faces to the geometry
  geometry.faces.push(faceOne, faceTwo);
  return geometry;
}

function updateFaceVertexUvs(geometry, j) {  
  // Identify the relative width and height of the subimages
  // within the image atlas
  var relativeW = image.width / atlas.width;
  var relativeH = image.height / atlas.height;

  // Identify this subimage's offset in the x dimension
  // An xOffset of 0 means the subimage starts flush with
  // the left-hand edge of the atlas
  var xOffset = (j % atlas.cols) * relativeW;
  
  // Identify this subimage's offset in the y dimension
  // A yOffset of 0 means the subimage starts flush with
  // the bottom edge of the atlas
  var yOffset = 1 - (Math.floor(j/atlas.cols) * relativeH) - relativeH;

  // Determine the faceVertexUvs index position
  var faceIdx = 2 * (j%1024);

  // Use the xOffset and yOffset (and the knowledge that
  // each row and column contains only 32 images) to specify
  // the regions of the current image. Use .set() if the given
  // faceVertex is already defined, due to a bug in updateVertexUvs:
  // https://github.com/mrdoob/three.js/issues/7179
  if (geometry.faceVertexUvs[0][faceIdx]) {
    geometry.faceVertexUvs[0][faceIdx][0].set(xOffset, yOffset)
    geometry.faceVertexUvs[0][faceIdx][1].set(xOffset + relativeW, yOffset)
    geometry.faceVertexUvs[0][faceIdx][2].set(xOffset + relativeW, yOffset + relativeH)
  } else {
    geometry.faceVertexUvs[0][faceIdx] = [
      new THREE.Vector2(xOffset, yOffset),
      new THREE.Vector2(xOffset + relativeW, yOffset),
      new THREE.Vector2(xOffset + relativeW, yOffset + relativeH)
    ]
  }
  // Map the region of the image described by the lower-left, 
  // upper-right, and upper-left vertices to `faceTwo`
  if (geometry.faceVertexUvs[0][faceIdx+1]) {
    geometry.faceVertexUvs[0][faceIdx+1][0].set(xOffset, yOffset)
    geometry.faceVertexUvs[0][faceIdx+1][1].set(xOffset + relativeW, yOffset + relativeH)
    geometry.faceVertexUvs[0][faceIdx+1][2].set(xOffset, yOffset + relativeH)
  } else {
    geometry.faceVertexUvs[0][faceIdx+1] = [
      new THREE.Vector2(xOffset, yOffset),
      new THREE.Vector2(xOffset + relativeW, yOffset + relativeH),
      new THREE.Vector2(xOffset, yOffset + relativeH)
    ]
  }
  return geometry;
}

function buildMesh(geometry, material, idx) {
  // Convert the geometry to a BuferGeometry for additional performance
  //var geometry = new THREE.BufferGeometry().fromGeometry(geometry);
  // Combine the image geometry and material into a mesh
  var mesh = new THREE.Mesh(geometry, material);
  // Store this image's index position in the mesh
  mesh.userData.idx = idx;
  // Set the position of the image mesh in the x,y,z dimensions
  mesh.position.set(0,0,0)
  // Add the image to the scene
  scene.add(mesh);
  // Save this mesh
  meshes.push(mesh);
  return mesh;
}

/**
* Update Geometries with new VertexUvs and materials
**/

function updateMesh(size, idx) {
  // Update the appropriate material
  meshes[idx].material = materials[size][idx];
  meshes[idx].material.needsUpdate = true;
  // Update the facevertexuvs
  for (var j=0; j<atlas.cols*atlas.rows; j++) {
    meshes[idx].geometry = updateFaceVertexUvs(meshes[idx].geometry, j);
  }
  meshes[idx].geometry.uvsNeedUpdate = true;
  meshes[idx].geometry.verticesNeedUpdate = true;
}

/**
* Helpers
**/

function setImageAndAtlasSize(size) {
  // Identify the subimage size in px (width/height) and the
  // size of the image as it will be displayed in the map
  image = { width: size,  height: size, shownWidth: 64, shownHeight: 64 };
  
  // Identify the total number of cols & rows in the image atlas
  atlas = { width: 2048, height: 2048, cols: 2048/size, rows: 2048/size };
}

/**
* Add Controls
**/

var controls = new THREE.TrackballControls(camera, renderer.domElement);

/**
* Add Raycaster
**/

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();

function onMouseMove( event ) {
  // Calculate mouse position in normalized device coordinates
  // (-1 to +1) for both components
  mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
  mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}

function onClick( event ) {
  // Determine which image is selected (if any)
  var selected = raycaster.intersectObjects( scene.children );
  // Intersecting elements are ordered by their distance (increasing)
  if (!selected) return;
  if (selected.length) {
    selected = selected[0];
    console.log('clicked', selected.object.userData.idx)
  }
}

window.addEventListener('mousemove', onMouseMove)
window.addEventListener('click', onClick)

/**
* Handle window resizes
**/

window.addEventListener('resize', function() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize( window.innerWidth, window.innerHeight );
  controls.handleResize();
});

/**
* Render!
**/

// The main animation function that re-renders the scene each animation frame
function animate() {
requestAnimationFrame( animate );
  raycaster.setFromCamera( mouse, camera );
  renderer.render( scene, camera );
  controls.update();
}
animate();
* {
  margin: 0;
  padding: 0;
  background: #000;
  color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js"></script>
<script src="https://s3.amazonaws.com/duhaime/blog/tsne-webgl/assets/js/texture-loader.js"></script>
<script src="https://s3.amazonaws.com/duhaime/blog/tsne-webgl/assets/js/trackball-controls.js"></script>
<div id='loader'>0%</div>

最佳答案

您可以潜在地使用多种 Material 和几何组(或者在您的情况下, Material 索引)。

这取决于您的纹理尺寸缩放比例 1::1。换句话说,如果您的第一个分辨率为 32x64 尺寸,那么该分辨率的两倍应具有 64x128 尺寸。 UV 是基于百分比的,因此从一个分辨率的图像移动到另一个分辨率的同一图像“有效”。

此时,您真的只需要更改纹理图像源即可。但听起来你不想那样做。因此,我们需要立即将所有纹理分配给同一个 Mesh。 Three.js 让这一切变得非常简单......

var myMesh = new THREE.Mesh(myGeometry, [ material1, material2, material3 ]);

请注意, Material 参数被定义为一个数组。每种 Material 都有不同的纹理,在您的情况下是不同分辨率的图像。

现在,调试您的 Mesh。在 goemetry 属性下,您会看到一个名为 faces 的属性,它是 Face3 对象的数组。每个面都有一个名为 materialIndex 的属性。这是面部对 Material 数组的引用。

当您到达要触发更改的点(例如您的相机距离网格有一定距离)时,您可以更改 Material 索引,然后触发网格更改其 Material :

var distance = camera.position.distanceTo(myMesh.position);
if(distance < 50){
  myMesh.faces.forEach(function(face){
    face.materialIndex = 2;
  });
}
else if(distance => 50 && distance < 100){
  myMesh.faces.forEach(function(face){
    face.materialIndex = 1;
  });
}
else{
  myMesh.faces.forEach(function(face){
    face.materialIndex = 0;
  });
}
myMesh.groupsNeedUpdate = true;

最后一行 (myMesh.groupsNeedUpdate = true;) 告诉渲染器 Material 索引已更改,因此需要为渲染更新 Material 。

关于javascript - Three.js:逐步增加纹理分辨率的策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47465975/

相关文章:

javascript - 3DS Max => ThreeJs。导出场景

javascript - 鼠标悬停时 Jquery 交换图片库

javascript - 如何使用 google app 脚本从 html 表获取数据以运行

javascript - 单击时交换多张图像,但一次交换一张

javascript - 为什么 three.js 在一些简单的成员函数上使用 IIFE 模式?

javascript - Three.js PointerLockControls和Physisjs碰撞检测

javascript、jquery时间格式

ggplot2 中面的旋转

r - 识别栅格 map 上的线性特征并使用 R 返回线性形状对象

r - 从较大的栅格堆栈创建栅格堆栈的许多子集