I'm just getting started with three.js, and having some issues extruding some 2d shapes.
I have a GeoJSON file containing all the counties in the US. I'm using d3.js and a d3.geo.albersUSa() projection to convert each latitude/longitude into a list of X/Y coordinates to make a THREE.Shape that I'm then extruding and drawing. This seems to work OK for most counties.
The issue I'm seeing is that some subset of counties either fail to extrude or extrude incorrectly with the following sequences of warnings:
Warning, unable to triangulate polygon!
Duplicate point 653.4789181355854:204.0166729191409
Either infinite or no solutions!
Its finite solutions.
Either infinite or no solutions!
Too bad, no solutions.
I'm not sure I understand exactly what the issue is -- as far as I can tell, there's nothing special about these particular shapes. Am I doing something wrong, or is this an issue with the extrusion code in three.js?
For example here are some missing counties:
Also notice the triangular 'hourglass' missing pieces in Texas: these look like some counties which were only half rendered (they ended up as triangles instead of rectangles or squares?)
Larger
Apologies for the huge code dump, I tried to pare it down as much as possible.
setup:
/* initialize the scene, camera, light, and background plane */
var Map = function(params) {
this.width = params.width;
this.height = params.height;
this.container = params.target || document.body;
this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setSize(this.width, this.height);
this.renderer.setClearColorHex(0x303030, 1.0);
this.container.appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(45, this.width / this.height,
1, 10000);
this.scene = new THREE.Scene();
this.scene.add(this.camera);
this.camera.position.z = 550;
this.camera.position.x = 0;
this.camera.position.y = 550;
this.camera.lookAt(this.scene.position);
this.projection = d3.geo.albersUsa()
.scale(1000)
.translate([250, 0]);
var pointLight = new THREE.PointLight(0xFFFFFF);
pointLight.position.x = 800;
pointLight.position.y = 800;
pointLight.position.z = 800;
var plane = new THREE.Mesh(
new THREE.PlaneGeometry(10000, 10000, 10, 10),
new THREE.MeshLambertMaterial({color: 0xffffff})
);
plane.rotation.x = -Math.PI/2;
this.scene.add(pointLight);
this.scene.add(plane);
};
rendering:
/* given a GeoJSON Feature, return a list of Vector2s
* describing where to draw the feature, using the provided projection. */
function path(proj, feature) {
if (feature.geometry.type == 'Polygon') {
return polygonPath(proj, feature.geometry.coordinates);
} else if (feature.geometry.type == 'MultiPolygon') {
return multiPolygonPath(proj, feature.geometry.coordinates);
}
}
/* a GeoJSON Polygon is a set of 'rings'. The first ring is the shape of the polygon.
* each subsequent ring is a hole inside that polygon. */
function polygonPath(proj, rings) {
var list = [];
var cur = [];
$.each(rings, function(i, ring) {
cur = [];
$.each(ring, function(i, coord) {
var pts = proj(coord);
cur.push(new THREE.Vector2(pts[0], pts[1]));
});
list.push(cur);
});
return list;
}
/* a GeoJSON MultiPolgyon is just a series of Polygons. */
function multiPolygonPath(proj, polys) {
var list = [];
$.each(polys, function(i, poly) {
list.push(polygonPath(proj, poly));
});
return list;
}
/* for each feature, find it's X/Y Path, create shape(s) with the required holes,
* and extrude the shape */
function renderFeatures(proj, features, scene, isState) {
var color = 0x33ccff;
$.each(features, function(i, feature) {
var polygons = path(proj, feature);
if (feature.geometry.type != 'MultiPolygon') {
polygons = [polygons];
}
$.each(polygons, function(i, poly) {
var shape = new THREE.Shape(poly[0]);
if (poly.length > 1) {
shape.holes = poly.slice(1).map(function(item) { return new THREE.Shape(item); });
}
var geom = new THREE.ExtrudeGeometry(shape, { amount: 20, bevelEnabled: false });
var c = new THREE.Mesh(geom, new THREE.MeshLambertMaterial({color: color}) );
c.rotation.x = Math.PI/2;
c.translateX(-290);
c.translateZ(50);
c.translateY(5);
scene.add(c);
});
});
}
Map.prototype.renderCounties = function() {
$.getJSON('/data/us-counties.json', function(json) {
renderFeatures(this.projection, json.features, this.scene, false);
this.renderer.render(this.scene, this.camera);
}.bind(this));
}
It looks like the points of the polygon are in the wrong order.
Related
I'm trying to play around with particles in three.js but, there's a problem with converting obj file (3D model) into particles in three.js. The following is the code snippets. I tried but, all failed.
Is there anyone who can help correcting the errors or provide with any examples of getting vertices/particles from a 3D model in obj?
Thanks a lot.
var p_geom = new THREE.Geometry();
var p_material = new THREE.ParticleBasicMaterial({
color: 0xFFFFFF,
size: 1.5
});
var loader = new THREE.OBJLoader();
loader.load( 'human.obj',function(object){
object.traverse( function(child){
if ( child instanceof THREE.Mesh ) {
// child.material.map = texture;
var scale = 10.0;
object.attributes.position.array.forEach(function() {
p_geom.vertices.push(new THREE.Vector3(this.x * scale, this.y * scale, this.z * scale));
})
}
});
scene.add(p)
});
p = new THREE.ParticleSystem(
p_geom,
p_material
);
You are using an outdated code reference. With recent three.js version, the code looks more like the following:
const loader = new THREE.OBJLoader();
loader.load('human.obj', function(object) {
const vertices = [];
object.traverse(function(child) {
if (child.isMesh) {
vertices.push(...child.geometry.attributes.position.array);
}
});
const p_geom = new THREE.BufferGeometry();
const p_material = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1.5
});
p_geom.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const p = new THREE.Points(p_geom, p_material);
p.scale.set(10, 10, 10);
scene.add(p)
});
I made a new THREE.PlaneBufferGeometry(100, 100, 100, 100); and have been able to update position of vertices to change the mesh's shape like following:
I achieved this by following this discussion: Threejs drag points
What am I looking for?
I want to be able to extrude a face (grab 4 vertices), so I achieve something like this:
I want to keep it all part of the same mesh, to keep it clean, because I will be exporting it as a single mesh with the ColladaExporter.
Edit
In order to achieve this, I would need to clone vertex and extrude them upwards. This means, adding 4 new vertex and connecting them together.
I tried this:
var geo = new THREE.PlaneBufferGeometry(1, 1, 1, 1);
geo.rotateX(-Math.PI * 0.5);
geo.translate(0,0.5,0);
//And the merge them together
var newplane = BufferGeometryUtils.mergeBufferGeometries([plane, geo]);
newplane = BufferGeometryUtils.mergeVertices(newplane,1);
And I got this:
I was hoping all vertices merged with the plane, leaving a flat plane. I did this for testing purposes, but it only merged one corner.
I started building a "cube" with multiple and placing them in the right spot, to then apply again BufferGeometryUtils.mergeVertices, but the vertices don't seem to merge correctly:
Edit 2 / Progress
I managed to create a PlaneBufferGeometry and extrude it by manually modifying the vertexes and normals, as told in: https://threejs.org/docs/#api/en/core/BufferGeometry
Extruded plane has all vertices connected, so whenever I drag one vertex it drags a whole piece, the problem now is that I need to connect these new vertices to the original grid to avoid this:
Goal is to merge all vertices, now I need to find a way to merge the base plane with the new extruded piece.
Edit 3 / Done
I made it, I will post answer when I have some time. I spent all day long on these today, and already very tired.
Not sure if that's what you need, but here's the modified example from the answer you referred to (please notice the difference in mouseMove implementation). I've extended that for two points only, but I believe you should get the idea:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(1.25, 7, 7);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var geometry = new THREE.PlaneBufferGeometry(10, 10, 10, 10);
geometry.rotateX(-Math.PI * 0.5);
var plane = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
wireframe: true,
color: "red"
}));
scene.add(plane);
var points = new THREE.Points(geometry, new THREE.PointsMaterial({
size: 0.25,
color: "yellow"
}));
scene.add(points);
var raycaster = new THREE.Raycaster();
raycaster.params.Points.threshold = 0.25;
var mouse = new THREE.Vector2();
var intersects = null;
var plane = new THREE.Plane();
var planeNormal = new THREE.Vector3();
var currentIndex = null;
var planePoint = new THREE.Vector3();
var dragging = false;
window.addEventListener("mousedown", mouseDown, false);
window.addEventListener("mousemove", mouseMove, false);
window.addEventListener("mouseup", mouseUp, false);
function mouseDown(event) {
setRaycaster(event);
getIndex();
dragging = true;
}
function mouseMove(event) {
if (dragging && currentIndex !== null) {
setRaycaster(event);
raycaster.ray.intersectPlane(plane, planePoint);
var indicesToMoveUp = [currentIndex-1, currentIndex];
var delta_x = geometry.attributes.position.getX(currentIndex) - planePoint.x;
geometry.attributes.position.setXYZ(currentIndex, planePoint.x, planePoint.y, planePoint.z);
geometry.attributes.position.needsUpdate = true;
var old_x_neighbour = geometry.attributes.position.getX(currentIndex - 1);
geometry.attributes.position.setY(currentIndex-1, planePoint.y);
geometry.attributes.position.setZ(currentIndex-1, planePoint.z);
geometry.attributes.position.setX(currentIndex-1, old_x_neighbour - delta_x);
geometry.attributes.position.needsUpdate = true;
}
}
function mouseUp(event) {
dragging = false;
currentIndex = null;
}
function getIndex() {
intersects = raycaster.intersectObject(points);
if (intersects.length === 0) {
currentIndex = null;
return;
}
currentIndex = intersects[0].index;
setPlane(intersects[0].point);
}
function setPlane(point) {
planeNormal.subVectors(camera.position, point).normalize();
plane.setFromNormalAndCoplanarPoint(planeNormal, point);
}
function setRaycaster(event) {
getMouse(event);
raycaster.setFromCamera(mouse, camera);
}
function getMouse(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.min.js"></script>
I have a canvas for the game world and a canvas for the display screen. I also have a polygon with nodes V(x,y) to serve as a viewport that follows the player and his rotation. I would like to know how to clip from the game world along the polygon, rotate and draw to the smaller canvas.`
//main looping function
var requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();
//joystick setup
var leftManager = null;
var rightManager = null;
//precalculated math
var twoPi = Math.PI*2;
var halfPi = Math.PI/2;
var thirdOfCircleInRadians = twoPi/3;
//game canvas setup
var gameCvs = document.getElementById('gameCanvas');
gameCvs.width = 480;
gameCvs.height = 320;
//gameCvs.width - 960;
//gameCvs.height = 640;
var gameCtx = gameCvs.getContext("2d");
//game loop
var lastTime = 0;
function main() {
var now = Date.now();
var dt = lastTime==0? 0.016 : (now - lastTime) / 1000.0;
update(dt);
render(dt);
lastTime = now;
requestAnimFrame(main);
}
//collision class shorthand
var V = SAT.Vector;
var C = SAT.Circle;
var P = SAT.Polygon;
var R = new SAT.Response();
P.prototype.draw = function (ctx,type) {
ctx.save();
switch(type){
case 'van': ctx.fillStyle = "rgba(66, 66, 66, 0.5)"; break;
case 'col': ctx.fillStyle = "rgba(0, 0, 0, 1.0)"; break;
default: ctx.fillStyle = "rgba(0, 0, 0, 1.0)"; break;
}
ctx.translate(this.pos.x, this.pos.y);
ctx.beginPath();
var points = this.calcPoints;
ctx.moveTo(points[0].x, points[0].y);
var i = points.length;
while (i--) ctx.lineTo(points[i].x, points[i].y);
ctx.closePath();
//stroke to see through camera, when camera is not drawn use fill
ctx.stroke();
//ctx.fill();
ctx.restore();
};
//first for collisions, second for vanity. first is black, second is grey
var O = function(colPolygon,vanPolygon){
this.colPolygon = colPolygon;
this.vanPolygon = vanPolygon;
this.visible = false;
};
var objectVendor = function(type,position){
switch(type){
case 'tree':
return new O(new P(position,[
new V(10.5,19.5),
new V(20.5,9.5),
new V(23,-4),
new V(15,-16.5),
new V(-4,-19.5),
new V(-18,-14.5),
new V(-23,-0.5),
new V(-18.5,14.5),
new V(-8,20)
]),new P(position,[
new V(21,39),
new V(41,19),
new V(46,-8),
new V(30,-33),
new V(-8,-39),
new V(-36,-29),
new V(-46,-1),
new V(-37,29),
new V(-16,40)]));
break;
default: return false; break;
}
return false;
}
//Camera and Player Polygons
var cameraPoly = new P(new V(0,0),[
new V(-240,-160),
new V(240,-160),
new V(240,160),
new V(-240,160)
]);
var player = new P(new V(0,0),[
new V(5,2.5),
new V(7.5,2),
new V(7.5,-2),
new V(5,-2.5),
new V(-5,-2.5),
new V(-7.5,-2),
new V(-7.5,2),
new V(-5,2.5)
]);
//players start position on the screen, and starting angle, init velocity
player.pos = new V(240,160);
player.setAngle(1);
//players velocity for movement
player.vel = new V(0,0);
var world = {
objects: [],
visibleObjects: [],
worldCvs: null,
worldCtx: null,
init: function(){
//set up world canvas
this.worldCvs = document.createElement('canvas');
this.worldCvs.width = 480;
this.worldCvs.height = 480;
this.worldCtx = this.worldCvs.getContext("2d");
//populate world with stuff
this.objects.push(objectVendor('tree',new V(100,100)));
this.objects.push(objectVendor('tree',new V(150,200)));
this.objects.push(objectVendor('tree',new V(75,300)));
},
update: function(dt){
this.visibleObjects = [];
cameraPoly.setAngle(player.angle);
//cameraPoly.pos = player.pos;
cameraPoly.pos = new V(player.pos.x+(110*Math.cos(player.angle+halfPi)),player.pos.y+(110*Math.sin(player.angle+halfPi)));
//update objects to mark if they are in view
var i = this.objects.length;
while(i--){
if(SAT.testPolygonPolygon(this.objects[i].vanPolygon, cameraPoly, R)){
this.visibleObjects.push(this.objects[i]);
}
}
//}
},
draw: function(dt){
this.worldCtx.setTransform(1,0,0,1,0,0);
this.worldCtx.clearRect(0,0,this.worldCvs.width,this.worldCvs.height);
player.draw(this.worldCtx);
var i = this.visibleObjects.length;
while(i--){
this.visibleObjects[i].colPolygon.draw(this.worldCtx,'col');
this.visibleObjects[i].vanPolygon.draw(this.worldCtx,'van');
}
//for testing
cameraPoly.draw(this.worldCtx);
/*
this.worldCtx.save();
this.worldCtx.beginPath();
var i = cameraPoly.calcPoints.length;
this.worldCtx.moveTo(cameraPoly.calcPoints[0].x,cameraPoly.calcPoints[0].y);
while(i--){
this.worldCtx.lineTo(cameraPoly.calcPoints[i].x,cameraPoly.calcPoints[i].y);
}
this.worldCtx.clip();
this.worldCtx.restore();
*/
}
}
function render(dt){
gameCtx.setTransform(1,0,0,1,0,0);
gameCtx.clearRect(0,0,gameCvs.width,gameCvs.height);
world.draw();
//gameCtx.save();
//gameCtx.translate(cameraPoly.pos.x,cameraPoly.pos.y);
//gameCtx.translate(gameCtx.width/2,gameCtx.height/2);
//gameCtx.rotate(-player.angle+halfPi);
//gameCtx.translate(-world.worldCvs.width/2,-world.worldCvs.height/2);
gameCtx.drawImage(world.worldCvs,0,0);
//gameCtx.restore();
}
function update(dt){
world.update();
}
function init(){
//joystick setup
leftManager = nipplejs.create({
zone:document.getElementById("leftJoystick"),
color:"black",
size:75,
threshold:1.0,
position:{
top:"50%",
left:"50%"
},
mode:"static",
restOpacity:0.75,
});
rightManager = nipplejs.create({
zone:document.getElementById("rightJoystick"),
color:"black",
size:75,
threshold:1.0,
position:{
top:"50%",
right:"50%"
},
mode:"static",
restOpacity:0.75,
});
//joystick event setup
leftManager.get().on('move end', function(evt,data){
//console.log(evt);
//console.log(data);
});
rightManager.get().on('move end', function(evt,data){
//console.log(evt);
//console.log(data);
});
world.init();
main();
}
init();
`
I'm using libraries SAT.js and nipplejs.js currently.
Typically this is done in a little different of a way than you seem to be thinking of it. Instead of thinking about the viewport existing somewhere in the world, you should think about the viewport being fixed and the world being transformed behind it; you don't copy part of the world to the viewport, you draw the world offset and rotated by a certain amount, and only draw the parts that are inside the viewport. Matrices are an easy and common way to represent this transformation. You may want to read more about them here.
In practice, this would just amount to changing your existing call to worldCtx.setTransform() at the beginning of each draw frame. That link has information about how to calculate a good transform matrix, and you can find similar resources all over the place since it's pretty standard math.
In particular, you'll want to multiply a rotation and a translation matrix. Translation matrices are only possible if you use a matrix with higher-order than your coordinate space; for 2D, a 3x3 matrix, and for 3D, a 4x4 matrix. You could instead choose to just add some offset to your coordinates as you draw them, but worldCtx.setTransform already takes a matrix with a 3rd column for putting flat offsets into.
Changing the render function to the following will solve the problem, just rushing myself and didn't think things through very well.
`
function render(dt){
gameCtx.setTransform(1,0,0,1,0,0);
gameCtx.clearRect(0,0,gameCvs.width,gameCvs.height);
world.draw();
gameCtx.translate(gameCvs.width/2,gameCvs.height/2);
gameCtx.rotate(-player.angle+Math.PI);
gameCtx.translate(-cameraPoly.pos.x,-cameraPoly.pos.y);
gameCtx.drawImage(world.worldCvs,0,0);
}`
What this is doing is resetting any transformations on the context, clearing it for a new redrawing, creating the world canvas, translating to display center, rotating by the proper amount for reference point, translating to reference center point on negative axis to move game canvas proper amount so that drawing at 0,0 is in the correct location. Thank you for the reference material!
I'm working on a Three.js chart that basically represents a bunch of images in a 2D plane.
Right now the individual images are each 32px by 32px segments of larger 2048px by 2048px image atlas files. I want to increase the size of those individual images when users zoom in to particular regions of the scene. For example, if users start to zoom in on the images in the far right region of the space, I plan to update the 32px by 32px individual images in that region with 64px by 64px images with the same content (to show more detail).
My question is: what's the Three.js way to accomplish this goal?
My flat-footed plan is to load the higher-resolution assets, map them to the proper geometry coordinates, then simply delete the old mesh with 32px subimages and add the new mesh with 64px subimages. I originally thought I could just update the texture/material for an extant geometry, but I've read that one shouldn't use textures larger than 2048px by 2048px, and a geometry with n points won't allow me to continually increase the fidelity of the images in that geometry without surpassing that maximum texture size.
I would be very grateful for any insight Three.js veterans can offer on how they would approach this task!
Full code:
/**
* 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>
You can potentially use mutli-materials and geometry groups (or in your case, material indices).
This depends on your texture dimensions scaling 1::1. In other words, if your first resolution as dimensions 32x64, then double that resolution should have dimensions of 64x128. UVs are percentage-based, so moving from an image at one resolution to the same image at another resolution "just works".
At this point, you really only need to change the texture image source. But it sounds like you don't want to do that. So instead, we need to assign ALL of your textures to the same Mesh at once. Three.js makes this really easy...
var myMesh = new THREE.Mesh(myGeometry, [ material1, material2, material3 ]);
Notice that the material parameters is defined as an array. Each material has a different texture, which in your case are the different resolution images.
Now, debug into your Mesh. Under the goemetry property, you'll see an a property called faces, which is an array of Face3 objects. Each face has a property named materialIndex. This is the face's reference to the array of materials.
When you reach a point where you want to trigger a change (such as your camera being a certain distance from a mesh), you can change the material index, then trigger the mesh to change its 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;
The last line (myMesh.groupsNeedUpdate = true;) tells the renderer that the material indices changed, so it will need to update the materials for the render.
Perhaps you could use THREE.LOD. It basically allows you to define different meshes for a range of distances. The meshes would be the same Quads, but you could change their materials to use different textures...
Here is the LOD example in the THREE.js web.
Hope it helps!!
I'm moving a camera through a scene that contains an obj I've loaded as a mesh, and I want to detect if the camera has collided with any of the walls of my obj.
I've based my code off this threejs example: http://threejs.org/examples/misc_controls_pointerlock.html, and tried to apply your example here: http://stemkoski.github.io/Three.js/Collision-Detection.html - but I can't seem to get a collision.
My example is here
and the relevant javascript is here:
If anyone can point me in the right direction on how to detect a collision, I'd be grateful. Here's the relevant piece of code:
var objects = [];
var oLoader = new THREE.OBJLoader();
//oLoader.load('models/chair.obj', function(object, materials) {
oLoader.load('models/model-for-threejs.obj', function(object, materials) {
// var material = new THREE.MeshFaceMaterial(materials);
var material = new THREE.MeshLambertMaterial({ color: 0x000000 });
//var material = new THREE.MeshBasicMaterial({wireframe: true});
object.traverse( function(child) {
if (child instanceof THREE.Mesh) {
//objects.push(child); //MH - not sure if the object needs to be added here or not, but if I do, this really bogs down things down
}
});
object.position.x = 0;
object.position.y = 12;
object.position.z = 0;
scene.add(object);
objects.push(object);
});
}
var curPos = controls.getObject().position; //gives the position of my camera
raycaster.ray.origin.copy( curPos );
//raycaster.ray.origin.y -= 10;
raycaster.ray.origin.z +=10; //guessing here, but since I'm moving in 2d space (z / x), maybe I should be moving my ray ahead in z space?
var intersections = raycaster.intersectObjects( objects ); //
var isOnObject = intersections.length > 0;
if (isOnObject){ console.log('collide' }; //MH - nothing happening here
Raycaster's intersect object takes an Object3D with children, and has a flag for recursion.
https://github.com/mrdoob/three.js/blob/master/src/core/Raycaster.js#L33
So it should look like this:
var intersections = raycaster.intersectObjects( yourRootObject3D, true );