使用Three.js绘制一个虚拟城市

这篇文章解释了如何使用代码来编写一座3D立体“城市”。这个代码是由@ mrdoob最新发布的演示Demo。我发现这个演示的算法很优雅,是一个简单而有效的解决方案,所以我发了一个帖子解释它。

关于算法的一些评论

在我们将关注焦点置于问题的细节之前,把握下问题的整体和全局是很有帮助的。这个3D虚拟城市所使用的算法是完全由程序所生成的,这意味着整个城市
是动态建立,而不参考任何模板。这个算法相当优雅,且不超过100行javascript代码。这个算法的原理是怎么样的呢?简而言之,每一个建筑是一个
立方体,他们得到随机的大小和位置。足够简单吗?听起来好像不切实际,但事实就是这样的,当你从城市底部往上看时就会发现这个秘密。

从性能的角度来看,所有的建筑都合并成一个单一的几何形状,用一个单一的材料。这是做法是非常有效的,因为没有着色器切换和绘图调用命令。

为了提高真实感,通过模拟自然光使用了vertexColor小把戏。在这个城市,在街道级别里你可以看到来自其他建筑物的阴影,所以建筑物的底部比顶部暗,我们可以采用vertexColor来重现这样的效果。我们采取建筑物的底部顶点,并使其比顶部更暗。

让我们开始吧

我们将逐步解释那100行代码:(1)生成建筑的基础几何形状 ;(2)在城市的合适位置放置建筑物;(3)使用vertexColor技巧模拟环境光和阴影;(4)合并所有的建筑物,这样整个城市可以在一次性绘制。不多说,让我们开始吧!

生成建筑的基础几何形状

我们首先需要建立构建城市建筑的基础几何形状,它会重复使用多次,然后构建起整个城市。所以我们建立了一个简单的CubeGeometry对象。

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;

我们设置好了建筑物的位置/旋转/缩放等属性。现在让我们设置它的颜色,以及如何使用它来模拟阴影。

使用vertexColor技巧模拟环境光和阴影

在建筑丛生的大城市,建筑的底部往往比顶端更暗。这是因为太阳光线照射到建筑物顶部比底部更容易,而且在建筑物底部往往由来自其它建筑物的阴影,这 是在图形编程中称之为环境光遮蔽(Ambient Occlusion)。使用ThreeJs,使得我们可以很轻易分配一种给定颜色给一个顶点,这最终将改变表面的最终颜色。我们要去利用这个特性来模拟建 筑物的底部的阴影。首先我们定义的向光面和背光面的基本色。

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作为上方顶点的颜色,使用baseColor乘上shaddow作为下方顶点的颜色来模拟环境光遮蔽的效果。

// 以baseColor作为参考设置上方顶点和下方顶点的颜色
var topColor  = baseColor.clone().multiply( light );
var bottomColor = baseColor.clone().multiply( shadow );
// 每个面的每个顶点指定vertexColor的属性值
var geometry  = buildingMesh.geometry;
for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) {
  if ( j === 2 ) {
    // 如果这个面是顶面
    geometry.faces[ j ].vertexColors = [ baseColor, baseColor, baseColor, baseColor ];
  } else {
    //如果这个面是侧面
    geometry.faces[ j ].vertexColors = [ topColor, bottomColor, bottomColor, topColor ];
  }
}

合并所有的建筑物

为了构建我们的城市,我们需要合并20000个建筑物在一起,为每个建筑物应用之前描述的过程。我们已经看到,降低绘图命令调用可以获取很好的性能。在这里,所有建筑物共用相同的材料,我们可以采用一个单一的几何物体来合并这些建筑。

var cityGeometry= new THREE.Geometry();
for( var i = 0; i < 20000; i ++ ){
  // 为每个建筑设置位置、旋转、大小和颜色
  // ... 

  // 合并所有建筑为单一的cityGeometry,可以有力的提升性能
  THREE.GeometryUtils.merge( cityGeometry, buildingMesh );
}

现在,我们得到了整个城市的一个大几何形状,让我们采用它来构建Mesh对象。

// build the mesh
var material  = new THREE.MeshLambertMaterial({
  map           : texture,
  vertexColors  : THREE.VertexColors
});
var mesh = new THREE.Mesh(cityGeometry, material );

这个Mesh对象就是我们想要构建的城市。还差一步,我们还需要添加纹理。

用程序生成纹理对象

在这里,我们要为每个建筑物的侧面生成纹理,以添加建筑的真实感。它采用交替的窗户和楼层进行。每个窗户通过不同的噪声模拟每个房间的光线明暗变化。

首先,你建一个Canvas画布,不需要很大,32×64就够了。

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 );
  }
}

现在我们已经有纹理了 32* 64 ,我们需要增加它的分辨率。首先,让我们创建一个更大的画布,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对象,

var texture   = new THREE.Texture( generateTexture() );
texture.anisotropy  = renderer.getMaxAnisotropy();
texture.needsUpdate = true;

这就是最后一步了!现在,你知道如何绘制一个虚拟的3D城市了,爽!下面是完整的代码。

// build the base geometry for each building
var geometry = new THREE.CubeGeometry( 1, 1, 1 );
// translate the geometry to place the pivot point at the bottom instead of the center
geometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );
// get rid of the bottom face - it is never seen
geometry.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 );
// buildMesh
var buildingMesh= new THREE.Mesh( geometry );

// base colors for vertexColors. light is for vertices at the top, shaddow is for the ones at the bottom
var 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 texture
var texture       = new THREE.Texture( generateTexture() );
texture.anisotropy = renderer.getMaxAnisotropy();
texture.needsUpdate    = true;

// build the mesh
var 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 );

演示地址

项目地址

时间: 2024-10-11 16:46:22

使用Three.js绘制一个虚拟城市的相关文章

基于D3.js 绘制一个折柱混合图

测试题目:使用 D3 绘制一个折柱混合图,示例数据如下: data = [ ["时间", "销售额", "增长率(%)"], ["一月", 27506, 20.8], ["二月", 24399, 5.4], ["三月", 23120, 22], ["四月", 22053, 0.4], ["五月", 21221, 3.1], ["六月&qu

SA:利用SA算法解决TSP(数据是14个虚拟城市的横纵坐标)问题——Jason niu

%SA:利用SA算法解决TSP(数据是14个虚拟城市的横纵坐标)问题--Jason niu X = [16.4700 96.1000 16.4700 94.4400 20.0900 92.5400 22.3900 93.3700 25.2300 97.2400 22.0000 96.0500 20.4700 97.0200 17.2000 96.2900 16.3000 97.3800 14.0500 98.1200 16.5300 97.3800 21.5200 95.5900 19.4100

放弃antd table,基于React手写一个虚拟滚动的表格

缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反而有所上升. 客观地说,antd是开源的,UI设计得比较美观(甩出其他组件库一条街),而且是蚂蚁金服的体验技术部(一堆p7,p8,p9,基本都是大牛级的)在持续地开发维护,质量可以信任. 不过,antd虽好,但一些组件在某一些场景下,是很不适用的.例如,以表格形式无限滚动地展示大量数据(1w+)时,

使用D3.js绘制地图并打点

本篇简单介绍一下使用D3.js绘制地图,并更具经纬度在地图打点.最后根据点生成voronoi图及其三角网. 下载地图geoJson文件 去网上下载要绘制地图的geoJson文件. 使用d3.json()加载地图文件,这里为了方便加载我把geoJson文件放在了js文件里. 绘制地图 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title>

[3] 用D3.js做一个简单的图表吧!

本人的个人博客为: www.ourd3js.com csdn博客为: blog.csdn.net/lzhlzz 转载请注明出处,谢谢. 前面说了几节,都是对文字进行处理,这一节中将用 D3.js 做一个简单的柱形图. 做柱形图有很多种方法,比如用 HTML 的 div 标签,或用 svg . 推荐用 SVG 来做各种图形.SVG 意为可缩放矢量图形(Scalable Vector Graphics),SVG 使用 XML 格式定义图像,不清楚什么是SVG的朋友请先在 w3cschools 学习下

用CSS3/JS绘制自己想要的按钮

我认为按钮的绘制分以下三个步骤 第一步,绘制按钮的轮廓 选择合适的html标签,设置轮廓的CSS /* html代码 */ <a href="#" class="button off"></a> body{ background-color: #E6C9B6; } /* CSS样式 */ /* 按钮轮廓 */ .button{ display: block; margin:100px auto; position: relative; wid

JS实现一个基本的打地鼠游戏

直入正题,用JS实现一个简单的打地鼠游戏 因为功能比较简单就直接裸奔JS了,先看看效果图,或者 在线玩玩 吧 如果点击颜色比较深的那个(俗称坏老鼠),将扣分50:如果点击颜色比较浅的那个(俗称好老鼠),将得分100 实现 老鼠好像有点难画,又不想用图片,就直接用CSS画个简单的图代表老鼠和坑吧 html结构 挺简单,用9个 li 标签代表坑,用9个 div 标签代表老鼠 <div class="container"> <h4>无聊打打地鼠</h4>

四、绘制一个点

上次我们介绍了如何在<canvas>中使用WebGL,以及几个基础的WebGL函数:实现了背景色的重置:为了扩展方便,我们把上次的代码做了些改动,将绘制图形的js独立成文件,这样我们只关注与这个js文件的编写:以后除非HTML文件发生变化,我们就跳过它,直接讨论JavaScript代码. 1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <met

Javascript实战开发:教你使用raphael.js绘制中国地图

最近的数据统计项目中要用到中国地图,也就是在地图上动态的显示某个时间段某个省份地区的统计数据,我们不需要flash,仅仅依靠raphael.js以及SVG图像就可以完成地图的交互操作.在本文中,我给大家分享如何使用js来完成地图交互. 先简单介绍下raphael.js,raphael.js是一个很小的javascript库,它可以在网页中实现绘制各种矢量图.各类图表.以及图像裁剪.旋转.运动动画等等功能.此外raphael.js还跨浏览器兼容,而且还兼容老掉牙的IE6啊.raphael.js的官