diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f8fa6a8..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Michele Guerini Rocco - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index beaf156..f857429 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -terrain -======= +# Terrain -Simple terrain generator +## Simple terrain generator +![Screenshot](screenshot.png) +## Info +A simple application which uses the diamond square algorithm to generate a terrain and renders it in OpenGL. + +### License +Dual licensed under the MIT and GPL licenses: +http://www.opensource.org/licenses/mit-license.php +http://www.gnu.org/licenses/gpl.html diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..ffe6f3f Binary files /dev/null and b/screenshot.png differ diff --git a/terrain.py b/terrain.py new file mode 100644 index 0000000..cb13e73 --- /dev/null +++ b/terrain.py @@ -0,0 +1,148 @@ +import random +import numpy +import pyglet +from pyglet.gl import * +from trackball_camera import TrackballCamera + + +class Terrain(): + def __init__(self, details): + self.size = 2 ** details + 1 + self.map = numpy.zeros((self.size, self.size)) + + def _square(self, x, y, size, offset): + average = sum([ + self.map[x - size][y - size], + self.map[x + size][y - size], + self.map[x + size][y + size], + self.map[x - size][y + size]]) / 4 + self.map[x][y] = average + offset + + def _diamond(self, x, y, size, offset): + average = sum([ + self.map[x][y - size], + self.map[x + size][y], + self.map[x][y + size], + self.map[x - size][y]]) / 4 + self.map[x][y] = average + offset + + def generate(self, high): + i, half = self.size - 1, (self.size - 1) / 2 + scale = high * self.size + + while i > 1: + for y in numpy.arange(half, self.size - 1, i): + for x in numpy.arange(half, self.size - 1, i): + self._square(x, y, half, scale / 2 * random.random()) + for y in numpy.arange(0, self.size - 1, half): + for x in numpy.arange((y + half) % i, self.size - 1, i): + self._diamond(x, y, half, scale / 2 * random.random()) + i /= 2 + half = i / 2 + scale = high * half * 2 + + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + self.terrain = None + self.camera = kwargs['camera'] + del kwargs['camera'] + super().__init__(*args, **kwargs) + + # Initialize OpenGL + glEnable(GL_DEPTH_TEST) + glDisable(GL_CULL_FACE) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + """Move camera/zoom on mouse drag""" + if buttons & pyglet.window.mouse.LEFT: + self.camera.mouse_roll( + self._norm(x, self.width), + self._norm(y, self.height)) + elif buttons & pyglet.window.mouse.RIGHT: + self.camera.mouse_zoom( + self._norm(x * 2, self.width), + self._norm(y * 2, self.height)) + + def on_mouse_press(self, x, y, button, modifiers): + """Move camera/zoom on mouse drag""" + if button == pyglet.window.mouse.LEFT: + self.camera.mouse_roll( + self._norm(x, self.width), + self._norm(y, self.height), + False) + elif button == pyglet.window.mouse.RIGHT: + self.camera.mouse_zoom( + self._norm(x * 2, self.width), + self._norm(y * 2, self.height), + False) + + def on_resize(self, width, height): + """Adjust drawing after window is resized""" + self.width = width + self.height = height + glViewport(0, 0, self.width, self.height) + self.on_show() + + def on_show(self): + """Set OpenGl config""" + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(40, self.width / self.height, 1, 400) + self.camera.update_modelview() + + def on_draw(self): + """Draw the current frame""" + if self.terrain is None: + return + self.clear() + map, size = self.terrain.map, self.terrain.size + + glPushMatrix() + glTranslatef(-size / 2, 0, -size / 2) + + # Draw terrain + for x in numpy.arange(size - 1): + for y in numpy.arange(size - 1): + glBegin(GL_TRIANGLE_STRIP) + glColor3f(map[x][y] / 20, map[x][y] / 20, map[x][y] / 20) + glVertex3f(x, map[x][y], y) + glVertex3f(x + 1, map[x + 1][y], y) + glVertex3f(x, map[x][y + 1], y + 1) + glVertex3f(x + 1, map[x + 1][y + 1], y + 1) + glEnd() + glPopMatrix() + + # Draw axis + glBegin(GL_LINES) + glColor3f(1, 0, 0) + glVertex3f(-size, 0, 0) + glVertex3f(size, 0, 0) + glColor3f(0, 1, 0) + glVertex3f(0, -size, 0) + glVertex3f(0, size, 0) + glColor3f(0, 0, 1) + glVertex3f(0, 0, -size) + glVertex3f(0, 0, size) + glEnd() + + def _norm(self, x, max_x): + """given x within [0,max_x], scale to a range [-1,1]""" + return (2 * x - float(max_x)) / float(max_x) + + def draw(self, terrain): + """Render the height map""" + self.terrain = terrain + + +def main(): + terrain = Terrain(5) + camera = TrackballCamera(150) + window = Window(caption="Terrain", resizable=True, + width=800, height=600, camera=camera) + terrain.generate(0.7) + window.draw(terrain) + pyglet.app.run() + +if __name__ == '__main__': + main() diff --git a/trackball_camera.py b/trackball_camera.py new file mode 100644 index 0000000..c09068a --- /dev/null +++ b/trackball_camera.py @@ -0,0 +1,338 @@ +"""trackball_camera.py - An OpenGL Trackball Camera Class for Pyglet + +by Roger Allen, July 2008 +roger@rogerandwendy.com + +A class for simple-minded 3d example apps. + +Usage: + +Initialize with a radius from the center/focus point: + + tbcam = TrackballCamera(5.0) + +After adjusting your projection matrix, set the modelview matrix. + + tbcam.update_modelview() + +On each primary mouse click, scale the x & y to [-1,1] and call: + + tbcam.mouse_roll(x,y,False) + +On each primary mouse drag, scale the x & y to [-1,1] and call: + + tbcam.mouse_roll(x,y) + +Mouse movements adjust the modelview projection matrix directly. + +""" + +__version__ = "1.0" + +# Code derived from the GLUT trackball.c, but now quite different and +# customized for pyglet. +# +# I simply wanted an easy-to-use trackball camera for quick-n-dirty +# opengl programs that I'd like to write. Finding none, I grabbed +# the trackball.c code & started hacking. +# +# Originally implemented by Gavin Bell, lots of ideas from Thant Tessman +# and the August '88 issue of Siggraph's "Computer Graphics," pp. 121-129. +# and David M. Ciemiewicz, Mark Grossman, Henry Moreton, and Paul Haeberli +# +# Note: See the following for more information on quaternions: +# +# - Shoemake, K., Animating rotation with quaternion curves, Computer +# Graphics 19, No 3 (Proc. SIGGRAPH'85), 245-254, 1985. +# - Pletinckx, D., Quaternion calculus as a basic tool in computer +# graphics, The Visual Computer 5, 2-13, 1989. +# +# Gavin Bell's code had this copyright notice: +# (c) Copyright 1993, 1994, Silicon Graphics, Inc. +# ALL RIGHTS RESERVED +# Permission to use, copy, modify, and distribute this software for +# any purpose and without fee is hereby granted, provided that the above +# copyright notice appear in all copies and that both the copyright notice +# and this permission notice appear in supporting documentation, and that +# the name of Silicon Graphics, Inc. not be used in advertising +# or publicity pertaining to distribution of the software without specific, +# written prior permission. +# +# THE MATERIAL EMBODIED ON THIS SOFTWARE IS PROVIDED TO YOU "AS-IS" +# AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, +# INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR +# FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON +# GRAPHICS, INC. BE LIABLE TO YOU OR ANYONE ELSE FOR ANY DIRECT, +# SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY +# KIND, OR ANY DAMAGES WHATSOEVER, INCLUDING WITHOUT LIMITATION, +# LOSS OF PROFIT, LOSS OF USE, SAVINGS OR REVENUE, OR THE CLAIMS OF +# THIRD PARTIES, WHETHER OR NOT SILICON GRAPHICS, INC. HAS BEEN +# ADVISED OF THE POSSIBILITY OF SUCH LOSS, HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE +# POSSESSION, USE OR PERFORMANCE OF THIS SOFTWARE. +# +# US Government Users Restricted Rights +# Use, duplication, or disclosure by the Government is subject to +# restrictions set forth in FAR 52.227.19(c)(2) or subparagraph +# (c)(1)(ii) of the Rights in Technical Data and Computer Software +# clause at DFARS 252.227-7013 and/or in similar or successor +# clauses in the FAR or the DOD or NASA FAR Supplement. +# Unpublished-- rights reserved under the copyright laws of the +# United States. Contractor/manufacturer is Silicon Graphics, +# Inc., 2011 N. Shoreline Blvd., Mountain View, CA 94039-7311. + +import math +import copy +from pyglet.gl import * + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# a little vector library that is misused in odd ways below. +def v3add(src1, src2): + return [ src1[0] + src2[0], + src1[1] + src2[1], + src1[2] + src2[2] ] + +def v3sub(src1, src2): + return [ src1[0] - src2[0], + src1[1] - src2[1], + src1[2] - src2[2] ] + +def v3scale(v, scale): + return [ v[0] * scale, + v[1] * scale, + v[2] * scale ] + +def v3dot(v1, v2): + return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] + +def v3cross(v1, v2): + return [ (v1[1] * v2[2]) - (v1[2] * v2[1]), + (v1[2] * v2[0]) - (v1[0] * v2[2]), + (v1[0] * v2[1]) - (v1[1] * v2[0]) ] + +def v3length(v): + return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) + +def v3normalize(v): + try: + tmp = v3scale(v,1.0/v3length(v)) + return tmp + except ZeroDivisionError: + return v + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Some quaternion routines +def q_add(q1, q2): + """Given two quaternions, add them together to get a third quaternion. + Adding quaternions to get a compound rotation is analagous to adding + translations to get a compound translation. When incrementally + adding rotations, the first argument here should be the new rotation. + """ + t1 = v3scale(q1,q2[3]) + t2 = v3scale(q2,q1[3]) + t3 = v3cross(q2,q1) + tf = v3add(t1,t2) + tf = v3add(t3,tf) + tf.append( q1[3] * q2[3] - v3dot(q1,q2) ) + return tf + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def q_from_axis_angle(a, phi): + # a is a 3-vector, q is a 4-vector + """Computes a quaternion based on an axis (defined by the given vector) + and an angle about which to rotate. The angle is expressed in radians. + """ + q = v3normalize(a) + q = v3scale(q, math.sin(phi/2.0)) + q.append(math.cos(phi/2.0)) + return q + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def q_normalize(q): + """Return a normalized quaternion""" + mag = (q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]) + if mag != 0: + for i in range(4): + q[i] /= mag; + return q + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def q_matrix(q): + """return the rotation matrix based on q""" + m = [0.0]*16 + m[0*4+0] = 1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2]) + m[0*4+1] = 2.0 * (q[0] * q[1] - q[2] * q[3]) + m[0*4+2] = 2.0 * (q[2] * q[0] + q[1] * q[3]) + m[0*4+3] = 0.0 + + m[1*4+0] = 2.0 * (q[0] * q[1] + q[2] * q[3]) + m[1*4+1] = 1.0 - 2.0 * (q[2] * q[2] + q[0] * q[0]) + m[1*4+2] = 2.0 * (q[1] * q[2] - q[0] * q[3]) + m[1*4+3] = 0.0 + + m[2*4+0] = 2.0 * (q[2] * q[0] - q[1] * q[3]) + m[2*4+1] = 2.0 * (q[1] * q[2] + q[0] * q[3]) + m[2*4+2] = 1.0 - 2.0 * (q[1] * q[1] + q[0] * q[0]) + m[2*4+3] = 0.0 + + m[3*4+0] = 0.0 + m[3*4+1] = 0.0 + m[3*4+2] = 0.0 + m[3*4+3] = 1.0 + return m + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def project_z(r, x, y): + """Project an x,y pair onto a sphere of radius r OR a hyperbolic sheet + if we are away from the center of the sphere. + """ + d = math.sqrt(x*x + y*y) + if (d < r * 0.70710678118654752440): # Inside sphere + z = math.sqrt(r*r - d*d) + else: # On hyperbola + t = r / 1.41421356237309504880 + z = t*t / d + return z + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Trackball Camera Class +# +class TrackballCamera: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def __init__(self, radius=1.0): + """ initialize the camera, giving a radius from the focal point for + the camera eye. Update focal point & up via the update_modelview call. + """ + # the quaternion storing the rotation + self.rot_quat = [0,0,0,1] + # the last mouse update + self.last_x = None + self.last_y = None + # camera vars + self.cam_eye = [0.,0.,radius] + self.cam_focus = [0.,0.,0.] + self.cam_up = [0.,1.,0.] + # in add_quat routine, renormalize "sometimes" + self.RENORMCOUNT = 97 + self.count = 0 + # Trackballsize should really be based on the distance from the center of + # rotation to the point on the object underneath the mouse. That + # point would then track the mouse as closely as possible. This is a + # simple example, though, so that is left as an Exercise for the + # Programmer. + self.TRACKBALLSIZE = 0.8 + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def mouse_roll(self, norm_mouse_x, norm_mouse_y, dragging=True): + """When you click or drag the primary mouse button, scale the mouse + x & y to the range [-1.0,1.0] and call this routine to roll the trackball + and update the modelview matrix. + + The initial click should set dragging to False. + """ + if dragging: + norm_mouse_quat = self._rotate(norm_mouse_x, norm_mouse_y) + self.rot_quat = q_add(norm_mouse_quat,self.rot_quat) + self.count += 1 + if (self.count > self.RENORMCOUNT): + self.rot_quat = q_normalize(self.rot_quat) + self.count = 0 + self.update_modelview() + self.last_x = norm_mouse_x + self.last_y = norm_mouse_y + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def mouse_zoom(self, norm_mouse_x, norm_mouse_y, dragging=True): + """When you click or drag a secondary mouse button, scale the mouse + x & y to the range [-1.0,1.0] and call this routine to change the + trackball's camera radius and update the modelview matrix. + + The initial click should set dragging to False. + """ + if self.last_x: + dx = norm_mouse_x - self.last_x + dy = norm_mouse_y - self.last_y + norm_mouse_r_delta = 20.0*math.sqrt(dx*dx+dy*dy) + if dy > 0.0: + norm_mouse_r_delta = -norm_mouse_r_delta + if dragging: + self.cam_eye[2] = self.cam_eye[2] + norm_mouse_r_delta + if self.cam_eye[2] < 1.0: + self.cam_eye[2] == 1.0 + self.update_modelview() + self.last_x = norm_mouse_x + self.last_y = norm_mouse_y + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def update_modelview(self,cam_radius=None,cam_focus=None,cam_up=None): + """Given a radius for the trackball camera, a focus-point 3-vector, + another 3-vector the points 'up' combined with the current + orientation of the trackball, update the GL_MODELVIEW matrix. + """ + if cam_radius: + self.cam_eye[2] = cam_radius + if cam_focus: + self.cam_focus = cam_focus + if cam_up: + self.cam_up = cam_up + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + gluLookAt( + self.cam_eye[0],self.cam_eye[1],self.cam_eye[2], + self.cam_focus[0],self.cam_focus[1],self.cam_focus[2], + self.cam_up[0],self.cam_up[1],self.cam_up[2] + ) + # rotate this view by the current orientation + m = self._matrix() + mm = (GLfloat * len(m))(*m) # FIXME there is prob a better way... + glMultMatrixf(mm) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def _matrix(self): + """return the rotation matrix for the trackball""" + return q_matrix(self.rot_quat) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def _rotate(self, norm_mouse_x, norm_mouse_y): + """Pass the x and y coordinates of the last and current positions of + the mouse, scaled so they are in the range [-1.0,1.0]. + + Simulate a track-ball. Project the points onto the virtual + trackball, then figure out the axis of rotation, which is the cross + product of LAST NEW and O LAST (O is the center of the ball, 0,0,0) + Note: This is a deformed trackball-- is a trackball in the center, + but is deformed into a hyperbolic sheet of rotation away from the + center. This particular function was chosen after trying out + several variations. + """ + # handle special case + if (self.last_x == norm_mouse_x and self.last_y == norm_mouse_y): + # Zero rotation + return [ 0.0, 0.0, 0.0, 1.0] + + # + # First, figure out z-coordinates for projection of P1 and P2 to + # deformed sphere + # + last = [self.last_x, self.last_y, project_z(self.TRACKBALLSIZE,self.last_x,self.last_y)] + new = [norm_mouse_x, norm_mouse_y, project_z(self.TRACKBALLSIZE,norm_mouse_x,norm_mouse_y)] + + # + # Now, we want the cross product of LAST and NEW + # aka the axis of rotation + # + a = v3cross(new,last) + + # + # Figure out how much to rotate around that axis (phi) + # + d = v3sub(last,new) + t = v3length(d) / (2.0*self.TRACKBALLSIZE) + # Avoid problems with out-of-control values... + if (t > 1.0): t = 1.0 + if (t < -1.0): t = -1.0 + phi = 2.0 * math.asin(t) + + return q_from_axis_angle(a,phi) +