133 行代码实现质感地形

本文由  伯乐在线  -  abell123  翻译自  Hunter Lofti 。欢迎加入 技术翻译小组 。转载请参见文章末尾处的要求。

程序员们都喜欢创造一些东西,但是,还有什么会比创建一个世界更让人感到惊喜?想想Minecraft, Terragen, Skyrim,以及以前的每一个都会使用一些生成分形地形的飞行模拟器。今天,我们要来探索如何使用漂亮而又简单的QPSO算法 (diamond-square algorithm),到时,你也可以扮演上帝![Demo] [Source]

程序员往往是懒惰的(从经验来说的话),而懒惰的一个很好地“副作用”就是这真的是可以避免一些(重复)工作的很不错的方式。既然这样, 与其花上乏味的几个小时来创建可能是很蹩脚的岩石表面,不如我们在思想上教会电脑岩石到底是什么。为了达到我们的目的,我们会生成分形或者形状,而这些形状会以越来越小的变化不断重复。

我并不能以某种方式来证明地形确实是分形的,但是这种方法看起来真的很不错,因此你可以信任这种方法。

立体地图

我们会将我们的地形存储为一个简单地立体地图:一个由地形在任意给定的x,y坐标上的高度值所组成的二维数组。这是一个比较简单的数据结构,用我们喜欢的canvas,webgl,interpretive dance等技术都可以来渲染这些高度值。最大的限制是我们不能在地形中表示有垂直的洞的形状,比如洞穴,隧道或者桥梁。

1
2
3
4
5
function Terrain(detail) { 
   this .size = Math.pow(2, detail) + 1; 
   this .max = this .size - 1; 
   this .map = new Float32Array( this .size * this .size); 
}

对任何尺寸的网格你都可以应用上面的算法来生成地图,但是对于一个由2的整数幂加1的网格组成方形来说它是最简单的。我们将使用x、y和z轴相同大小的值,在一个多维数据集中实现我们的地形。我们把相关的细节(detail)(即网格的数量)转化成了2的整数幂加1,因此更多的网格数量需要有更大的数据集。

对应的算法

想法是这样的:取一个平面的方形。把它分成4个子方形,然后把这4个子方形的中心向上或向下随机的偏移一定量。把这些子方形再分成更多的子方形并且重复上面的步骤,每一次都将偏移的量减少,这样第一次的偏移会有最大的效果而后面的偏移都会提供更小的细节(起伏程度)。

这就是中点置换算法(Midpoint displacement algorithm)。我们的菱形算法基于类似的原则,但是生成了看起来会更自然的结果。与其只是把方格分成更多的子方格,不如在分成子方格与分成子的菱形方格之间做个替换。

1.设置各个角的坐标

首先,我们要设置各个角的坐标值来作为“种子”值,它会影响后面的呈现。我们会将所有的角落从数据集的一半的位置开始:

1
2
3
4
this .set(0, 0, self.max / 2); 
this .set( this .max, 0, self.max / 2); 
this .set( this .max, this .max, self.max / 2); 
this .set(0, this .max, self.max / 2);

2.将地图分块

现在,我们将会递归的来看立体地图的越来越小的分块。在每一个分块的过程中,我们会把地图分成方块,并在方形阶段更新它们的中心点。然后,我们会把地图分成菱形,并在菱形阶段再次更新它们的中心点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
divide( this .max); 
function divide(size) { 
   var x, y, half = size / 2; 
   var scale = roughness * size; 
   if (half < 1) return
   for (y = half; y < self.max; y += size) { 
     for (x = half; x < self.max; x += size) { 
       square(x, y, half, Math.random() * scale * 2 - scale); 
    
  
   for (y = 0; y <= self.max; y += half) { 
     for (x = (y + half) % size; x <= self.max; x += size) { 
       diamond(x, y, half, Math.random() * scale * 2 - scale); 
    
  
   divide(size / 2); 
}

saele变量保证了随着我们分块的次数的增多,偏移量是不断减小的。对于每一次分块,我们将当前的size变量与roughness相乘,roughness决定了我们的地形是平滑的(该变量值趋近于0时)还是起伏的(该变量值趋近于1时)。

3.形状

两种形状(方形和菱形)的工作机制是类似的,但是要从不同的点来绘制数据。在方形阶段,要在应用随机偏移之前获取四个角的坐标的平均值,在菱形阶段要在执行随机偏移之前获取四个边缘点的坐标的平均值。

1
2
3
4
5
6
7
8
9
function diamond(x, y, size, offset) { 
   var ave = average([ 
     self.get(x, y - size),      // top 
     self.get(x + size, y),      // right 
     self.get(x, y + size),      // bottom 
     self.get(x - size, y)       // left 
   ]); 
   self.set(x, y, ave + offset); 
}

渲染

算法只是给了我们数据,我们可以用很多种方式来渲染数据。我们将整合一连串的渲染技巧来渲染一个位于canvas元素上的栅格化的,等距的,3d形式的地形图上。

从back到front

首先,我们将创建嵌套的循环从我们地图的“back”(y = 0) 到“front”(y=this.size)来绘制矩形。如果你要渲染一个简单地,平的,自顶向下的方形,那么要执行的循环是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
for ( var y = 0; y < this .size; y++) { 
   for ( var x = 0; x < this .size; x++) { 
     var val = this .get(x, y); 
     var top = project(x, y, val); 
     var bottom = project(x + 1, y, 0); 
     var water = project(x, y, waterVal); 
     var style = brightness(x, y, this .get(x + 1, y) - val); 
     rect(top, bottom, style); 
     rect(water, bottom, 'rgba(50, 150, 200, 0.15)' ); 
  
}

光亮和阴影

我们对于集合映射的原始方法提供了一个很好的视觉文理。通过比较当前的高度值和下一个点的高度值,我们会找到一个坡度。坡度高的一侧我们用较亮的矩形来填充,另一侧则用较暗的矩形来填充。

1
2
var b = ~~(slope * 50) + 128; 
return [ 'rgba(' , b, ',' , b, ',' , b, ',1)' ].join( '' );

等轴投影

我们可以从正面来绘制每一样东西,但是,在将方块转为3d之前,先将它转为菱形看起来会更有趣。等轴投影将左上角和右下角在视图的中间对齐。

1
2
3
4
5
6
function iso(x, y) { 
   return
     x: 0.5 * (self.size + x - y), 
     y: 0.5 * (x + y) 
   }; 
}

透视投影

我们将使用一个同样简单的3d投影转换我们的x,y,z值为在二维视角屏幕上的平面图像。

所有的透视投影的基本想法都是用水平和垂直的位置除以深度,那样的话更高的深度的渲染就会更接近于原点(例如,越远的物体就会看起来越小)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function project(flatX, flatY, flatZ) { 
     var point = iso(flatX, flatY); 
     var x0 = width * 0.5; 
     var y0 = height * 0.2; 
     var z = self.size * 0.5 - flatZ + point.y * 0.75; 
     var x = (point.x - self.size * 0.5) * 6; 
     var y = (self.size - point.y) * 0.005 + 1; 
     return
       x: x0 + x / y, 
       y: y0 + z / y 
     }; 
  
};

把所有的内容整合起来

首先,我们用我们所期望的细小平面创建了一个地形实例。然后我们生成了它的立体地图,提供了一个位于0和1之间的roughness值。最后,我们把地形绘制到了canvas上。

1
2
3
var terrain = new Terrain(9); 
terrain.generate(0.7); 
terrain.draw(canvasContext, width, height);

试试看

请点击下面链接查看最终效果:来自另一个世界的地形(otherworldly terrain)

接下来

如果你跟我一样,这个简单的算法会让你渴望去创造一个在线的自制梦幻风景,一个基于飞行器的第一人称射击游戏,模拟钓鱼或者一个大型多人在线角色扮演游戏等等。这个单一的立方体式的,基于canvas的demo非常需要扩展。

下面的几项我希望你能去尝试:

1.用WebGl渲染;

2.跟随高度的变化,高度越小的地形越平滑(像沙子),高度越大的越崎岖;

3.投射暗影,而不是简单地基于斜坡来产生阴影;

4.添加一个功能,生成洞穴和隧道。

按照惯例,你还可以在这里看到这个想法。

相关的工作

现在有很多人在研究这个算法,并且他们创建了很多很多很酷的东西。而且,黑客新闻讨论也展示了很多相关的真的非常不可思议的例子。这里有几个比较突出的:

  • WebGL rendering implementation by callum
  • Objective C implementation by Chris Cieslak
  • Processing implementation by Jerome Herr
  • Heightmap-based raycaster by namuol
  • Procedural demo entry explanation by Inigo Quilez
  • Fractional Brownian Motion by rbaravaelle
  • Polygonal game map generation by Red Blob Games

讨论

可以在 Hacker News 参与讨论。

转载: 133 行代码实现质感地形相关推荐

  1. 133 行代码实现质感地形

    本文由  伯乐在线  -  abell123  翻译自  Hunter Lofti .欢迎加入 技术翻译小组 .转载请参见文章末尾处的要求. 程序员们都喜欢创造一些东西,但是,还有什么会比创建一个世界 ...

  2. 133行代码实现质感地形(js,基于canvas)

    原文地址:http://www.playfuljs.com/realistic-terrain-in-130-lines/ 写于2014年5月5日 程序员们都喜欢创造一些东西,但是,还有什么会比创建一 ...

  3. [导入][转载]5行代码实现无缝滚动

    汗,今天无意中看到的,5行代码实现了无缝滚动,想起现在普通用的2个div替换的方法,简直是.. 程序代码 appendChild实现的无缝滚动 <style> *{border:1px s ...

  4. 转载:谢谢原作者:块设备驱动实战基础篇一 (170行代码构建一个逻辑块设备驱动)

    1   内核块设备驱动基础学习与实战 1.1 设备驱动IO架构初探 操作系统是如何将数据读到缓冲区的,发生了什么?我们带着这样的问题,粗略走一下read调用系统过程,希望这个初探,可以唤起大家研究操作 ...

  5. 几行代码轻松搞定网页的简繁转换(转载)

    对网页进行简繁字体转换的方法一般有两种:一是使用<简繁通>这样的专业软件,另外一种是制作两套版本的网页.显然,这两种方法都较为麻烦,而且专业软件一般不能用于免费的空间.笔者在这里给大家提供 ...

  6. 手把手入门神经网络系列(2)_74行代码实现手写数字识别

    作者: 龙心尘&&寒小阳  时间:2015年12月.  出处:  http://blog.csdn.net/longxinchen_ml/article/details/5028124 ...

  7. $《第一行代码:Android》读书笔记——第6章 数据持久化

    主要讲述了Android数据持久化的三种方式:文件存储.SharedPreference存储.SQLite数据库存储. (一)文件存储 其实Android中文件存储方式和Java的文件操作类似,就是用 ...

  8. 《第一行代码》封面诞生记

    <第一行代码>这本书,自2014年首次出版以来,已经过去了6年的时间.在这6年时间里,这本书一共升级改版了3次,包括即将出版但还未出版的这次. 虽然每次改版升级,书的封面都会或多或少发生些 ...

  9. 把三千行代码重构为15行

    2019独角兽企业重金招聘Python工程师标准>>> 如果你认为这是一个标题党,那么我真诚的恳请你耐心的把文章的第一部分读完,然后再下结论.如果你认为能够戳中您的G点,那么请随手点 ...

最新文章

  1. ubuntu/linuxmint如何添加和删除PPA源
  2. python【数据结构与算法】分治算法之大整数乘法
  3. [ZJOI2008] 树的统计(树链剖分)
  4. vue实现星级评价效果
  5. oracle中游标的使用
  6. vue router传参_新手使用vue-router传参时注意事项
  7. Redis系列教程(五):Redis哨兵、复制、集群的设计原理,以及区别
  8. 嵌入式c语言移植,嵌入式C语言位操作的移植方法
  9. stream流_最详细的JDK8新特性————Stream流
  10. Android 软键盘盖住输入框的问题
  11. 左传 —— 春秋左氏传
  12. 两种方法递归斐波那契数列
  13. java审批工作流,值得一读!
  14. glassfish java环境_GlassFish安装和配置详解
  15. WINDOWS 7 X86专业版SP1后续补丁包20150901(微软官方下载地址列表)
  16. 执念斩长河21年Q2生活心得
  17. origin如何绘制双y轴曲线_使用Origin软件绘制双y轴曲线图的过程
  18. windows7与linux,Windows7与Linux——操作系统大PK
  19. 帝国塔防2 empire defence 2的攻略
  20. ReadMe 好看指南

热门文章

  1. linux文件名长度限制6,linux和windows文件名长度限制问题
  2. 利用定时/计数器TO从P1输入周期为1s的方波,让发光二极管以1HZ闪烁,设晶振频率为12MHZ
  3. Go并发之同步异步、异步回调
  4. Unity零基础到进阶 ☀️| 音频源Audio Source组件 详解
  5. 小程序 - 手风琴折叠效果
  6. 嵌入式工程师的真情吐露:孤独,却有乐趣!
  7. 四、ubuntu20.04下找不到NVIDIA相关命令
  8. Android网速的显示
  9. 微信自动回复机器人使用手册
  10. 超详细的计算机网络基础知识总结 第四章:网络层