2017-11-24 31 views
0

私は基本的に2D平面上の画像の束を表すThree.jsチャートを作成しています。Three.js:テクスチャの解像度を徐々に上げる方針

現在、個別の画像は、2048×2048pxの画像アトラスファイルの32×32ピクセルのセグメントです。ユーザーがシーンの特定の領域を拡大するときに、個々の画像のサイズを大きくしたい。たとえば、ユーザーがスペースの右端にある画像を拡大し始めると、その領域の32ピクセル×32ピクセルの個別画像を同じ内容の64ピクセル×64ピクセルの画像(詳細を表示する)で更新する予定です。

私の質問です:この目標を達成するThree.jsの方法は何ですか?

高解像度のアセットをロードして適切なジオメトリ座標にマップし、その後、古いメッシュを32pxのサブ画像で削除し、新しいメッシュに64pxのサブ画像を追加するだけです。もともと、既存のジオメトリのテクスチャ/マテリアルを更新することができたと思っていましたが、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>

+0

** 1)** Stack Overflowは、codepen/jsfiddle/etcにリンクする際にコードを尋ねます。なぜなら、リンクがダウンすると、あなたの投稿はその例の文脈を失うからです。 ['Snippets'](https://stackoverflow.blog/2014/09/16/introducing-)を使用して、[最小、完全、および検証可能なサンプル](https://stackoverflow.com/help/mcve)を作成することを検討してください。 runnable-javascript-css-and-html-code-snippets /)を使用します。 ** 2)**アイロニーの壮大なケースでは、あなたのコードリンクが壊れています(404)。 ** 3)**あなたの '32x32'の各領域は、より大きなイメージを形成しますか?その場合、ジオメトリのUVを調整して、テクスチャのより大きなサンプルを取得することができます。 – TheJim01

+0

ありがとう@ TheJim01、私は上記のコードをインライン化しました。はい、私は大きな画像アトラスから画像を引っ張っていますが、画像解像度を徐々に更新したいと考えています。今では、あらかじめ割り当てられた(しかし空の)バッファを使って各メッシュに材料を追加し、それぞれのテクスチャデータを取得し、バッファを満たし、各メッシュの各要素のマテリアルインデックスを変更する必要があると思います。これが可能かどうかわかりません... – duhaime

答えて

1

潜在的には、相互マテリアルとジオメトリグループ(またはあなたの場合はマテリアルインデックス)を使用できます。

これは、テクスチャのディメンションのスケーリング1 :: 1によって異なります。つまり、最初の解像度が32x64の場合は、その解像度の2倍のサイズが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 && if(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;)は、レンダラに材料インデックスが変更されたことを伝えるので、レンダリングのために材料を更新する必要があります。

+0

非常に@ TheJim01ありがとう、これはまさに私が向けて進めてきたアプローチです。私はすぐに質問があります - どうして、groupsNeedUpdateを言う必要がありますか?この文脈では、グループは何を指していますか?私はあなたがその質問に共感できるどんな考えにも感謝します。 – duhaime

+1

フラグを設定すると、レンダリングにグループが変更されたことが通知され(フレームごとにすべての値が確認されやすくなります)、再度レンダリングする前に再評価する必要があります。この文脈における「グループ」は、ドローグループである。詳細は、['BufferGeometry.groups'](https://threejs.org/docs/#api/core/BufferGeometry.groups)を参照してください。注意:私は 'Geometry'を使っていても、' BufferGeometry'を参照しています...これらは、GLジオメトリバッファ、描画グループ、およびすべてを使いやすい抽象です。 – TheJim01

1

おそらく、あなたはTHREE.LODを使用することができます。基本的に、ある範囲の距離に対して異なるメッシュを定義することができます。メッシュは同じクワッドになりますが、異なるテクスチャを使用するようにマテリアルを変更することができます。 THREE.js WebにはLODの例があります。

希望すると助かります!

+0

あなたのメモに感謝します、私は前に 'LOD'を見ていませんでした。私は、前者がもっとコンパクトに見えるので、それぞれの材料バッファを持つ複数のメッシュよりも、追加の材料バッファをあらかじめ割り当てるメッシュを持つ方が良いと考えています。しかし、私は何かが欠けている? – duhaime

+0

これらのメッシュは重くはなく、四角形です。同じメッシュをLODで再利用できるかどうかはわかりませんが、少なくともLODを使用すると、距離を計算するためのコードを保存し、それに応じて素材を変更します。私はあなた自身で同じメカニズムを実装し、材料だけを変更できると確信しています。 – NullPointer

関連する問題