Three.js - 用100行javascript代码创建一座城市
来源:互联网 发布:淘宝开店实名认证失败 编辑:程序博客网 时间:2024/04/30 07:21
翻译有删改
原文链接:
http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/
算法评价
在深入细节之前先有一个全局的概念总是好的。该算法实现的整座城市都是动态建立的,而不是实现下载好的模型。算法写的非常优雅,创建一个3D城市仅仅用了100行代码。概括来讲:每栋建筑都是一个立方体,且它们的大小和位置随机。
从性能表现角度来说,所有的建筑都融合成一个单一的几何形状,有着单一的材质。没有材质上的变换和单一的绘图调用使得程序非常高效。
为了提高真实度,我们用一个简单的办法来模拟环境光的遮挡效果——使用vertexColor。在城市中,街道层有来自其他建筑物的阴影。所以建筑物的底部比顶部更暗。我们能通过设置vertexColor,使得建筑物底部顶点比顶部的颜色更暗,从而再现这个效果。
开始
我们一步步分析这100行代码:首先,我们为建筑物制造基本的几何形状;然后,我们使用这个几何形状来确定在城市的哪里放置建筑物,使用vertexColor来实现环境光遮挡的效果;然后,我们整合所有的建筑物来形成一个城市。因此,绘制整座城市只需要一个单一的绘制调用。最后,我们详细讲一下在渐进生成过程中建筑物的纹理。
让我们开始吧~!
为建筑物制造基本的几何形状
我们创建一个简单的立方体作为基本形状。
var geometry = new THREE.CubeGeometry( 1, 1, 1 );
我们将立方体的中轴点从中心变到底部。
geometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );
然后移除底部的面。这是一个优化的步骤,因为立方体底部的面永远不会被看到,所以可以移除。
geometry.faces.splice( 3, 1 );
现在我们为顶部的面修整UV贴图。将它们放到单一坐标(0,0)中。这样屋顶和地板颜色就一样了。因为建筑物的每个面都用一张贴图,所以调用绘制函数一次就好。
geometry.faceVertexUvs[0][2][0].set( 0, 0 );geometry.faceVertexUvs[0][2][1].set( 0, 0 );geometry.faceVertexUvs[0][2][2].set( 0, 0 );geometry.faceVertexUvs[0][2][3].set( 0, 0 );
好啦~现在我们有了单一建筑物的几何形状,接下来我们用建筑物来组合出一座城市吧~!
在城市的哪里放置建筑物
好吧,实际上我们是随机摆放它们的。虽然这样它们会发生冲突,但是在一个较低的位置漫游时看起来还好。
buildingMesh.position.x = Math.floor( Math.random() * 200 - 100 ) * 10;buildingMesh.position.z = Math.floor( Math.random() * 200 - 100 ) * 10;
然后给Y轴加一个随机的旋转。
buildingMesh.rotation.y = Math.random()*Math.PI*2;
然后我们通过改变mesh.scale来改变建筑物的大小。首先是宽度和深度。
buildingMesh.scale.x = Math.random()*Math.random()*Math.random()*Math.random() * 50 + 10;buildingMesh.scale.z = buildingMesh.scale.x
然后是高度。
buildingMesh.scale.y = (Math.random() * Math.random() * Math.random() * buildingMesh.scale.x) * 8 + 8;
在这里很多个Math.random()的连乘改变了结果的统计分布,使其更接近于0.现在,建筑物的位置、旋转和大小已经都设置好了。接下来设置它们的颜色和阴影仿真。
使用VertexColor模拟环境光遮挡
在graphic programming里面,环境光遮挡(ambientocclusion)可以被应用到很多个方面。
首先我们分别定义接收光源部分和阴影部分的基础色。这对于每个建筑物都是常量。
var light = new THREE.Color( 0xffffff )var shadow = new THREE.Color( 0x303050 )
接下来我们加一些随机值作为每个建筑物的变化色。
var value = 1 - Math.random() * Math.random();var baseColor = new THREE.Color().setRGB( value + Math.random() * 0.1, value, value + Math.random() * 0.1 );
现在我们需要给每个面的每个顶点分配.vertexColor。顶部面给baseColor,旁边的面给baseColor乘以顶部顶点的light和底部顶点的shaddow。以此来做简单的环境光遮挡效果。
// set topColor/bottom vertexColors as adjustement of baseColorvar topColor = baseColor.clone().multiply( light );var bottomColor = baseColor.clone().multiply( shadow );// set .vertexColors for each facevar geometry = buildingMesh.geometry;for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) { if ( j === 2 ) { // set face.vertexColors on root face geometry.faces[ j ].vertexColors = [ baseColor, baseColor, baseColor, baseColor ]; } else { // set face.vertexColors on sides faces geometry.faces[ j ].vertexColors = [ topColor, bottomColor, bottomColor, topColor ]; }}
现在单独的建筑物已经完全设置好了~!
用所有的建筑物组合成一座城市
为了制造我们的城市,我们需要整合20000个建筑物。所以我们用一个循环并且把循环中的建筑物都做以上处理。因为现在所有的建筑物都使用同样的材质,所以我们准备将它们整合到一个几何形状里。
var cityGeometry= new THREE.Geometry();for( var i = 0; i < 20000; i ++ ){ // set the position/rotation/color the building in the city // ... // merge it with cityGeometry - very important for performance THREE.GeometryUtils.merge( cityGeometry, buildingMesh );}
现在我们得到了城市的一整个几何形体,然后为这个大几何形体创建一个网格。
// build the meshvar material = new THREE.MeshLambertMaterial({ map : texture, vertexColors : THREE.VertexColors});var mesh = new THREE.Mesh(cityGeometry, material );
这个网格就是整座城市的模型。太棒啦~!接下来是最后一步,我们会讲解如何制作贴图。
建筑物贴图的渐进生成(procedural generation)
这里我们想要生成每个建筑物侧面的纹理。简单的说,这会展示出楼层的真实感和多样性。所以它在窗户行和楼层行之间交替进行。窗户行是带着微小噪音的黑色来模拟每间房间的光线变化。然后我们小心地将纹理升级以避免滤波。
首先创建一个小的canvas画布。
var canvas = document.createElement( 'canvas' );canvas.width = 32;canvas.height = 64;var context = canvas.getContext( '2d' );
然后染成白色。
context.fillStyle = '#ffffff';context.fillRect( 0, 0, 32, 64 );
现在我们开始在这个白色的表面。我们准备在上面绘制地板。一个窗户行,一个地板行然后进行循环。实际上,当表面已经是白色的时候,我们只需要绘制窗户行就可以了。为了绘制窗户行,我们要加一些随机值以模拟窗户中的光线变化。
for( var y = 2; y < 64; y += 2 ){ for( var x = 0; x < 32; x += 2 ){ var value = Math.floor( Math.random() * 64 ); context.fillStyle = 'rgb(' + [value, value, value].join( ',' ) + ')'; context.fillRect( x, y, 2, 1 ); }}
现在我们得到的纹理很小,为了放大后不模糊,我们关闭了.imageSmoothedEnabled效果。下面是代码:首先创建一个1024*512的大画布。
var canvas2 = document.createElement( 'canvas' );canvas2.width = 512;canvas2.height = 1024;var context = canvas2.getContext( '2d' );
然后关闭平滑。
context.imageSmoothingEnabled = false;context.webkitImageSmoothingEnabled = false;context.mozImageSmoothingEnabled = false;
现在把小的画布拷贝到大的里面。
context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height );
然后我们需要做的就是创建THREE.Texture。将anisotropie设置成一个较大的值以得到更好的效果。
var texture = new THREE.Texture( generateTexture() );texture.anisotropy = renderer.getMaxAnisotropy();texture.needsUpdate = true;
完整代码
// build the base geometry for each buildingvar geometry = new THREE.CubeGeometry( 1, 1, 1 );// translate the geometry to place the pivot point at the bottom instead of the centergeometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );// get rid of the bottom face - it is never seengeometry.faces.splice( 3, 1 );geometry.faceVertexUvs[0].splice( 3, 1 );// change UVs for the top face// - it is the roof so it wont use the same texture as the side of the building// - set the UVs to the single coordinate 0,0. so the roof will be the same color// as a floor row.geometry.faceVertexUvs[0][2][0].set( 0, 0 );geometry.faceVertexUvs[0][2][1].set( 0, 0 );geometry.faceVertexUvs[0][2][2].set( 0, 0 );geometry.faceVertexUvs[0][2][3].set( 0, 0 );// buildMeshvar buildingMesh= new THREE.Mesh( geometry );// base colors for vertexColors. light is for vertices at the top, shaddow is for the ones at the bottomvar light = new THREE.Color( 0xffffff )var shadow = new THREE.Color( 0x303050 )var cityGeometry= new THREE.Geometry();for( var i = 0; i < 20000; i ++ ){ // put a random position buildingMesh.position.x = Math.floor( Math.random() * 200 - 100 ) * 10; buildingMesh.position.z = Math.floor( Math.random() * 200 - 100 ) * 10; // put a random rotation buildingMesh.rotation.y = Math.random()*Math.PI*2; // put a random scale buildingMesh.scale.x = Math.random() * Math.random() * Math.random() * Math.random() * 50 + 10; buildingMesh.scale.y = (Math.random() * Math.random() * Math.random() * buildingMesh.scale.x) * 8 + 8; buildingMesh.scale.z = buildingMesh.scale.x // establish the base color for the buildingMesh var value = 1 - Math.random() * Math.random(); var baseColor = new THREE.Color().setRGB( value + Math.random() * 0.1, value, value + Math.random() * 0.1 ); // set topColor/bottom vertexColors as adjustement of baseColor var topColor = baseColor.clone().multiply( light ); var bottomColor = baseColor.clone().multiply( shadow ); // set .vertexColors for each face var geometry = buildingMesh.geometry; for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) { if ( j === 2 ) { // set face.vertexColors on root face geometry.faces[ j ].vertexColors = [ baseColor, baseColor, baseColor, baseColor ]; } else { // set face.vertexColors on sides faces geometry.faces[ j ].vertexColors = [ topColor, bottomColor, bottomColor, topColor ]; } } // merge it with cityGeometry - very important for performance THREE.GeometryUtils.merge( cityGeometry, buildingMesh );}// generate the texturevar texture = new THREE.Texture( generateTexture() );texture.anisotropy = renderer.getMaxAnisotropy();texture.needsUpdate = true;// build the meshvar material = new THREE.MeshLambertMaterial({ map : texture, vertexColors : THREE.VertexColors});var cityMesh = new THREE.Mesh(cityGeometry, material );function generateTexture() { // build a small canvas 32x64 and paint it in white var canvas = document.createElement( 'canvas' ); canvas.width = 32; canvas.height = 64; var context = canvas.getContext( '2d' ); // plain it in white context.fillStyle = '#ffffff'; context.fillRect( 0, 0, 32, 64 ); // draw the window rows - with a small noise to simulate light variations in each room for( var y = 2; y < 64; y += 2 ){ for( var x = 0; x < 32; x += 2 ){ var value = Math.floor( Math.random() * 64 ); context.fillStyle = 'rgb(' + [value, value, value].join( ',' ) + ')'; context.fillRect( x, y, 2, 1 ); } } // build a bigger canvas and copy the small one in it // This is a trick to upscale the texture without filtering var canvas2 = document.createElement( 'canvas' ); canvas2.width = 512; canvas2.height = 1024; var context = canvas2.getContext( '2d' ); // disable smoothing context.imageSmoothingEnabled = false; context.webkitImageSmoothingEnabled = false; context.mozImageSmoothingEnabled = false; // then draw the image context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height ); // return the just built canvas2 return canvas2;}
threex.proceduralcity扩展
这段代码被集成到一个易于复用的threex包里面:threex.proceduralcity。使用起来非常简单。
var city = new THREEx.ProceduralCity()scene.add(city)
- Three.js - 用100行javascript代码创建一座城市
- 用100行代Three.js代码创建一座城市
- Javascript省份城市(js代码)
- 【three.js】创建一个场景
- three.js 创建一个立方体
- Three.js学习创建物体
- 用Three.js根据两个三维点创建Cylinder
- 用Three.js创建一个简易的天空盒
- three.js入门篇(一)
- three.js学习计划(一)
- Three.js学习笔记(一)
- Three.js用鼠标控制场景移动的代码
- three.js 源码注释(一)./Three.js
- 城市二级联动js代码
- 城市连动纯js代码DEMO
- Three.js 中文手册-创建场景
- three.js第二篇【场景创建】
- three.js javascript 3d 超级引擎 !
- hdu 1028 Ignatius and the Princess III
- 高效求解平方根的倒数的函数实现
- 如何用Cocos2d-x创建lua项目以及lua项目如何调用cpp文件(图文讲解)
- Android Fragment IllegalStateException: Fragment not attached to Activity
- SpringMVC表单标签简介
- Three.js - 用100行javascript代码创建一座城市
- POJ 1979 Red and Black
- SVN中tag branch trunk用法详解
- 第九周项目二 分数的累加
- 全局Toast
- Java 并发编程(四)同步容器类
- 理解不同IO模式的生活示例
- Java Serialization
- 秒懂前缀、后缀、中缀