【问题标题】:Three.js: Strategies to progressively increase resolution of textureThree.js:逐步提高纹理分辨率的策略
【发布时间】:2017-11-24 03:34:05
【问题描述】:

我正在制作一个 Three.js 图表,它基本上代表了 2D 平面中的一堆图像。

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

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

我的计划是加载更高分辨率的资源,将它们映射到正确的几何坐标,然后简单地删除具有 32 像素子图像的旧网格并添加具有 64 像素子图像的新网格。我原本以为我可以只更新现有几何的纹理/材质,但我读过不应该使用大于 2048 像素乘 2048 像素的纹理,并且具有 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>

【问题讨论】:

  • 1) Stack Overflow 在链接到 codepen/jsfiddle/etc 时要求输入代码。因为当--not if--链接断开时,您的帖子将失去其示例的上下文。考虑使用Snippets 创建Minimal, Complete, and Verifiable example2) 具有讽刺意味的是,您的 codepen 链接已损坏 (404)。 3) 您的每个32x32 区域是否形成了更大的图像?如果是这样,您可以调整几何体的 UV 以获取更大的纹理样本。
  • 谢谢@TheJim01,我内联了上面的代码。是的,我正在从更大的图像图集中提取图像,但我想逐步更新图像分辨率。现在我想我需要使用预先分配的(但为空的)缓冲区为每个网格添加额外的材质,然后获取每个网格的纹理数据,填充缓冲区,然后更改每个网格中每个元素的材质索引,但我没有'不知道这是否可能......

标签: javascript three.js raster


【解决方案1】:

您可以潜在地使用多材质和几何组(或在您的情况下使用材质索引)。

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

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

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

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

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

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

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;) 告诉渲染器材质索引已更改,因此需要更新渲染材质。

【讨论】:

  • 非常感谢@TheJim01,这正是我一直在尝试的方法。不过我有一个简单的问题——为什么需要说 groupsNeedUpdate?在这种情况下,组指的是什么?如果您能就该问题分享任何想法,我将不胜感激。
  • 设置标志通知渲染组已更改(比每帧检查所有值更容易),并且必须在再次渲染之前重新评估它们。在此上下文中的“组”是绘制组。有关更多信息,请查看BufferGeometry.groups。请注意,我引用的是 BufferGeometry,即使您使用的是 Geometry... 它们都是 GL 几何缓冲区、绘图组等易于使用的抽象。
【解决方案2】:

也许你可以使用THREE.LOD。它基本上允许您为一系列距离定义不同的网格。网格将是相同的四边形,但您可以更改它们的材质以使用不同的纹理...... 这是 THREE.js 网络中的 LOD 示例。

希望对你有帮助!!

【讨论】:

  • 感谢您的留言,我之前没见过LOD。我的想法是,最好有一个网格,我为它预先分配额外的材质缓冲区,而不是多个网格,每个网格都有自己的材质缓冲区,因为前者看起来更紧凑。但我错过了什么吗?
  • 这些网格并不重,只是四边形。我真的不知道您是否可以在 LOD 中重复使用相同的网格,但至少,使用 LOD,您可以保存计算距离的代码并根据它更改材质。我相信您可以自己实现相同的机制并仅更改材料。
猜你喜欢
  • 1970-01-01
  • 2018-03-16
  • 2018-09-04
  • 2016-06-25
  • 1970-01-01
  • 1970-01-01
  • 2015-12-21
  • 2018-08-05
  • 2017-11-25
相关资源
最近更新 更多