Project 3b – Three.js (to be continued)
This post is to show off the changes I have made to the terrain from project 3 but it is by no means the final desired product. I’m moving on, for now, to explore different features of Three.js with the expectation that I will return to work on specific improvements.
If you have not read the previous part (project3a) I suggest to do so to avoid feeling extremely lost. This update includes adding plants to the land and creating water features (demo). The trees were made in collada by a friend, but with the system devised, one could add any threejs object that would then be placed sporadically about the surface. The water is just an extra plane added to the terrain, and the levels of the terrain are modified to create depth.
I created a separate function within the scripts on the index.html file to handle loading the tree and sending it to the terrain object to place. The abstract plant is loaded as a single object and then sent to a function attached to the terrain obj named addFoliage(object).
function loadTrees(land){ var colloader = new THREE.ColladaLoader(); colloader.load('../threejs/assets/tree.dae', function(mesh){ tree = mesh.scene; tree.rotateOnAxis(new THREE.Vector3(1,0,0), -0.5*Math.PI); tree.scale.x = tree.scale.y = tree.scale.z = .6; land.trees = land.addFoliage(tree); scene.add(land.trees); render(); }); }
I use the callback built into ColladaLoader to ensure that the tree object has fully loaded before attempting to add it to the landscape. I also had to rotate the tree and scale it slightly. The addFoliage(Obj) function returns the foliageGroup which I add to the terrain object under ‘trees’. This is then added to the scene. The loadTrees(land) function is called after the initial generation of the terrain obj inside the init() function:
terrain = new Terrain_Grid({ 'width':$('[name="size"] option:selected').val(), 'length':$('[name="size"] option:selected').val(), 'squareWidth':squareWidth, 'vary':$('[name="vary"] option:selected').val() }); scene.add(terrain.element); scene.add(terrain.water); loadTrees(terrain);
The addFoliage(Obj) function takes the terrain object and calls it’s addFoliage(). You can also see that terrain.water is added to the scene; this is the slightly opaque plane that represents water level. This function goes through a randomization sequence adding trees to certain squares. Each tree is added as a separate clone() of the original object to a THREE.Object3D which houses all objects as a single group. The .update() function on the terrain moves the trees to their new height also, however it doesn’t remove trees if they are moved to under the water-level. This means that if the terrain is only changed in variability, not size, there may be some trees underwater (tee-hee).
Here is the new returned object when generating a new Terrain_Grid:
return { element : terrainObj, geometry : terrainObj.geometry, water : waterPlane, update : function(size){ width = size.width || standards.width; //default 5 : x direction length = size.length || standards.length; // default 5 : z direction area = width*length; squareWidth = size.squareWidth || standards.squareWidth; halfSquareWidth = squareWidth/2; vary = size.vary || standards.vary; heights = generateHeight(width, length, vary); for(var i =0, k =0; i<area; i++){ for(var j=0; j<4; j++){ terrainObj.geometry.vertices[i*4+j].y = ~~(heights[i]*squareWidth/2); } while(foliageGroup.children[k] != undefined && foliageGroup.children[k].position.x < terrainObj.geometry.vertices[i*4+2].x && foliageGroup.children[k].position.z < terrainObj.geometry.vertices[i*4+1].z){ foliageGroup.children[k].position.y = ~~(heights[i]*squareWidth/2); console.log(foliageGroup.children[k].position.x, terrainObj.geometry.vertices[i*4+2].x, foliageGroup.children[k].position.y, terrainObj.geometry.vertices[i*4+2].y); k++; } } terrainObj.geometry.computeFaceNormals(); terrainObj.geometry.verticesNeedUpdate = true; terrainObj.geometry.normalsNeedUpdate = true; }, addFoliage : function(Obj){ var vertexArray = terrainObj.geometry.vertices; for(var index = 0, l = vertexArray.length; index*4 < l; index++){ //four vertices each face var x = index % width, z = ~~(index / width); //x and z in grid var xReal = vertexArray[index*4].x, yReal = vertexArray[index*4].y, zReal = vertexArray[index*4].z; //actual x,y,z var rand = Math.random(); if(yReal < 0){ //does nothing }else if(rand < .50){ var tempObj = Obj.clone(); tempObj.position.set(xReal + squareWidth*.2, yReal, zReal + squareWidth*.15); foliageGroup.add(tempObj); } else if(rand <.70){ var tempObj1 = Obj.clone(), tempObj2 = Obj.clone(); tempObj1.position.set(xReal + squareWidth*.3, yReal, zReal + squareWidth*.2); tempObj2.position.set(xReal + squareWidth*.1, yReal, zReal + squareWidth*.35); foliageGroup.add(tempObj1); foliageGroup.add(tempObj2); } else if(rand < .80){ var tempObj1 = Obj.clone(), tempObj2 = Obj.clone(), tempObj3 = Obj.clone(); tempObj1.position.set(xReal + squareWidth*.10, yReal, zReal + squareWidth*.2); tempObj2.position.set(xReal + squareWidth*.40, yReal, zReal + squareWidth*.35); tempObj3.position.set(xReal + squareWidth*.20, yReal, zReal + squareWidth*.15); foliageGroup.add(tempObj1); foliageGroup.add(tempObj2); foliageGroup.add(tempObj3); } } return foliageGroup; } }
The returned object contains the waterPlane as 'water' and the addFoliage(Obj) function. This new function takes a Threejs object that is already scaled and rotated. The for-loop is from 0 to vertexArray / 4 because each flat face contains 4 vertices. This index number is used to calculate x and z coordinates (y is the vertical direction here) and then retrieve the real positions of x, y, and z. A random number is created to determine how the trees should be placed. A future version will use perlin noise to determine placement instead so that trees will group together better. Currently, 50% of squares will get a single, off-center tree, 20% will get two trees, 10% will get three trees, and 20% will get no trees. The original object is simply cloned to make sure they are all separately manipulatable. They are then each positioned and then added to the foliageGroup. As stated before, the foliageGroup is returned from calling this function.
The averageHeight(data) function from the previous version was updated to create minor negative spaces so that the water would show through. Here is the updated version:
function averageHeight(data){ var totalH = 0, l = data.length; for(var i = 0; i<l; i++){ totalH += ~~data[i]; } var averageH = totalH / data.length; for(var i = 0; i<l; i++){ data[i] = data[i] - averageH*1.2; if(data[i] < 0){ if(data[i]< -averageH*.85){ data[i] = data[i] + averageH*.70; }else { data[i] = 0; } } } return data; }
Previously, after the averageH was subtracted, all negative heights would be reverted to 0. Instead, 120% of the average is subtracted and then if it is greater than 0.85*averageH under zero then only 0.7*averageH is added to the extreme negative. This results in very negative heights keeping below the water level.
Here is the generateWater() function:
function generateWater(){ var geometry = new THREE.PlaneGeometry( width * squareWidth - squareWidth*0.5, length* squareWidth - squareWidth*0.5); geometry.applyMatrix( new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) ); var element = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color:0x0000FF, opacity:.7, transparent: true})); element.position.set(width/2*squareWidth - squareWidth*.25,-squareWidth*.1,length/2*squareWidth - squareWidth*.25); return element; } waterPlane = generateWater();
This function creates the water plane using a PlaneGeometry of the exact same size as the terrain. It rotates the geometry to exist in the x-y plane. It creates the element from the geometry and a slightly opaque, blue plane. I also needed to reposition to align the two planes.
Altogether, the result is this demo. Additionally, you can download the source files: html file, Terrain_Grid.js, tree, ImprovedNoise.js, Detector.js, TrackballControls.js, three.min.js, and stats.min.js. Additionally, all of these are available through threejs.org in their examples.
Future plans include:
- Add Buildings
- Animations and better controls
- perlin noise tree placement
After my next project I plan to return to this and add the listed improvements.