2017-11-24 1 views
0

저는 기본적으로 2D 평면에서 이미지 묶음을 나타내는 Three.js 차트 작업을하고 있습니다.Three.js : 점진적으로 텍스처 해상도를 향상시키는 전략

지금은 개별 이미지가 더 큰 2048px, 2048px 이미지 아틀라스 파일의 32x32 픽셀 세그먼트입니다. 사용자가 장면의 특정 영역을 확대 할 때 개별 이미지의 크기를 늘리고 싶습니다. 예를 들어 사용자가 공간 오른쪽 끝에있는 이미지를 확대하기 시작하면 해당 지역의 32 픽셀 x 32 픽셀 개별 이미지를 64px x 64px 이미지 (동일한 세부 정보 표시)로 업데이트 할 계획입니다.

내 질문은 :이 목표를 달성하기위한 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/등으로 링크 할 때 코드를 요구합니다. 왜냐하면 __가 아니라 링크가 작동하지 않으면 게시물의 예가 상실되기 때문입니다. ['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

Thanks @ TheJim01, 나는 위의 코드를 인라인했다. 예, 저는 더 큰 이미지 아틀라스에서 이미지를 가져오고 있지만 이미지 해상도를 점진적으로 업데이트하려고합니다. 지금은 미리 할당 된 (하지만 비어있는) 버퍼를 가진 각 메쉬에 머티리얼을 추가 한 다음 각각의 텍스처 데이터를 가져와 버퍼를 채운 다음 각 메쉬의 각 요소에 대한 머티리얼 인덱스를 변경해야한다고 생각합니다. 이것이 가능한지 안다면 ... – duhaime

답변

1

잠재적으로 다중 재료 및 지오메트리 그룹 (또는 귀하의 경우 재료 지수)을 사용할 수 있습니다.

이것은 텍스처 크기 조정 1 :: 1에 따라 다릅니다. 즉, 첫 번째 해상도가 32x64 인 경우 그 해상도는 두 배인 64x128이어야합니다. UV는 백분율을 기반으로하므로 한 해상도의 이미지에서 다른 해상도의 같은 이미지로 이동하는 것이 "잘 작동합니다".

이 시점에서 실제로 텍스처 이미지 소스 만 변경하면됩니다. 하지만 그렇게하고 싶지 않은 것처럼 들리 네요. 따라서 대신 모든 텍스처를 동일하게 Mesh에 할당해야합니다. Three.js를 사용하면이 작업을 매우 쉽게 수행 할 수 있습니다.

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

Material 매개 변수는 배열로 정의됩니다. 각 머티리얼은 서로 다른 질감을 가지고 있습니다.이 경우 서로 다른 해상도 이미지가 있습니다.

이제 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, 이것은 정확히 제가 접해 왔던 접근법입니다. 그래도 빠른 질문이 있습니다. 왜 그룹을 말하지 않아도 되나요? 이 문맥에서 그룹은 무엇을 언급합니까? 나는 당신이 그 질문에 대해 공유 할 수있는 어떤 생각에 대해서 감사 할 것입니다. – duhaime

+1

플래그를 설정하면 그룹마다 변경된 (모든 프레임마다 모든 값을 확인하는 것보다 쉽다) 렌더링 된 것을 알리고 다시 렌더링하기 전에 다시 평가해야합니다. 이 컨텍스트에서 "그룹"은 드로잉 그룹입니다. 자세한 정보는 ['BufferGeometry.groups'] (https://threejs.org/docs/#api/core/BufferGeometry.groups)에서 확인하십시오. 공지 사항 저는 'Geometry'를 사용하고 있지만'BufferGeometry'를 참조하고 있습니다 ... 그들은 모두 GL 기하학 버퍼, 그리기 그룹 및 모두를 사용하기 쉬운 추상화입니다. – TheJim01

1

아마 당신은 THREE.LOD를 사용할 수 있습니다. 기본적으로 거리의 범위에 따라 서로 다른 메쉬를 정의 할 수 있습니다. 메쉬는 동일한 쿼드 (Quad) 일 것이지만, 서로 다른 텍스쳐를 사용하기 위해 머티리얼을 바꿀 수 있습니다 ... THREE.js 웹에 LOD 예제가 있습니다.

희망이 있습니다!

+0

메모 해 주셔서 감사합니다. 전에 'LOD'를 보지 못했습니다. 내 생각에, 하나의 메쉬를 갖는 것이 더 나았습니다. 전 메쉬가 더 작아 보임에 따라, 각각의 메쉬 버퍼를 사용하여 여러 메쉬보다 재료 버퍼를 미리 할당하는 것이 좋습니다. 그러나 나는 무엇인가 놓치고 있냐? – duhaime

+0

이 메쉬는 실제로 무겁지 않고 쿼드입니다. LOD에서 같은 메쉬를 재사용 할 수 있는지는 모르지만 적어도 LOD를 사용하면 거리를 계산하고 그에 따라 재질을 변경하는 코드를 저장해야합니다. 나는 당신이 당신 자신의 동일한 메커니즘을 구현할 수 있고 재료 만 바꿀 수있을 것이라고 확신합니다. – NullPointer