symmetry

diff src/terrain.cc @ 1:46fe847bba08

using goat3dgfx
author John Tsiombikas <nuclear@member.fsf.org>
date Tue, 25 Feb 2014 23:47:20 +0200
parents
children
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/terrain.cc	Tue Feb 25 23:47:20 2014 +0200
     1.3 @@ -0,0 +1,349 @@
     1.4 +#include <goat3dgfx/goat3dgfx.h>
     1.5 +#include "terrain.h"
     1.6 +#include "imago2.h"
     1.7 +
     1.8 +using namespace goatgfx;
     1.9 +
    1.10 +Terrain::Terrain()
    1.11 +{
    1.12 +	xsz = ysz = 0;
    1.13 +	dx = dy = 0;
    1.14 +	hmap = 0;
    1.15 +	scale = height_scale = 1;
    1.16 +
    1.17 +	dlist = dlist_norm = 0;
    1.18 +
    1.19 +	dlist_sub[0] = dlist_sub[1] = dlist_norm_sub[0] = dlist_norm_sub[1] = -1;
    1.20 +	dlist_norm_sz = -1.0;
    1.21 +}
    1.22 +
    1.23 +Terrain::~Terrain()
    1.24 +{
    1.25 +	if(hmap) {
    1.26 +		img_free_pixels(hmap);
    1.27 +	}
    1.28 +
    1.29 +	glDeleteLists(dlist, 2);
    1.30 +}
    1.31 +
    1.32 +void Terrain::set_scale(float s)
    1.33 +{
    1.34 +	if(s != scale) {
    1.35 +		scale = s;
    1.36 +		invalidate_mesh();
    1.37 +	}
    1.38 +}
    1.39 +
    1.40 +float Terrain::get_scale() const
    1.41 +{
    1.42 +	return scale;
    1.43 +}
    1.44 +
    1.45 +void Terrain::set_height_scale(float s)
    1.46 +{
    1.47 +	if(s != height_scale) {
    1.48 +		height_scale = s;
    1.49 +		invalidate_mesh();
    1.50 +	}
    1.51 +}
    1.52 +
    1.53 +float Terrain::get_height_scale() const
    1.54 +{
    1.55 +	return height_scale;
    1.56 +}
    1.57 +
    1.58 +bool Terrain::load(const char *fname)
    1.59 +{
    1.60 +	if(!(hmap = (float*)img_load_pixels(fname, &xsz, &ysz, IMG_FMT_GREYF))) {
    1.61 +		fprintf(stderr, "failed to load image: %s\n", fname);
    1.62 +		return false;
    1.63 +	}
    1.64 +
    1.65 +	dx = 1.0 / (float)(xsz - 1);
    1.66 +	dy = 1.0 / (float)(ysz - 1);
    1.67 +	return true;
    1.68 +}
    1.69 +
    1.70 +Vector2 Terrain::world_to_uv(const Vector2 &pt) const
    1.71 +{
    1.72 +	return pt  / scale + 0.5;
    1.73 +}
    1.74 +
    1.75 +Vector2 Terrain::uv_to_world(const Vector2 &pt) const
    1.76 +{
    1.77 +	return (pt - 0.5) * scale;
    1.78 +}
    1.79 +
    1.80 +float Terrain::lookup(int x, int y) const
    1.81 +{
    1.82 +	int px = x < 0 ? 0 : (x >= xsz ? x = xsz - 1 : x);
    1.83 +	int py = y < 0 ? 0 : (y >= ysz ? y = ysz - 1 : y);
    1.84 +	return hmap[py * xsz + px];
    1.85 +}
    1.86 +
    1.87 +float Terrain::get_height(const Vector2 &pt) const
    1.88 +{
    1.89 +	float fxsz = (float)xsz;
    1.90 +	float fysz = (float)ysz;
    1.91 +
    1.92 +	// the floor of x * xsz is the pixel x coordinate (e.g. [0.0, 1.0, 2.0, ... 127.0] for 128x128)
    1.93 +	float floorx = floor(pt.x * fxsz);
    1.94 +	float floory = floor(pt.y * fysz);
    1.95 +
    1.96 +	// (x0, y0) is the lower left pixel in normalized coords of the four pixels surrounding pt
    1.97 +	float x0 = floorx / fxsz;
    1.98 +	float y0 = floory / fysz;
    1.99 +
   1.100 +	// (u, v) is the relative position of pt within the 4pix square
   1.101 +	// so if it's exactly at the lower-right it'll be (0, 0), in the middle it'll be (0.5, 0.5) etc.
   1.102 +	float u = (pt.x - x0) * fxsz;
   1.103 +	float v = (pt.y - y0) * fysz;
   1.104 +
   1.105 +	// we need integer pixel coordinates to use lookup()
   1.106 +	int px = (int)floorx;
   1.107 +	int py = (int)floory;
   1.108 +
   1.109 +	// the heights at the 4 corners
   1.110 +	float h00 = lookup(px, py);
   1.111 +	float h10 = lookup(px + 1, py);
   1.112 +	float h01 = lookup(px, py + 1);
   1.113 +	float h11 = lookup(px + 1, py + 1);
   1.114 +
   1.115 +	// first interpolate along the top and bottom edges
   1.116 +	float top = lerp(h01, h11, u);
   1.117 +	float bot = lerp(h00, h10, u);
   1.118 +	// then vertically between the top/bot values
   1.119 +	return lerp(bot, top, v);
   1.120 +}
   1.121 +
   1.122 +Vector3 Terrain::get_normal(const Vector2 &pt) const
   1.123 +{
   1.124 +	// compute partial derivatives (slopes) in respect to X and Y using central differences
   1.125 +	float dfdx = get_height(Vector2(pt.x + dx, pt.y)) - get_height(Vector2(pt.x - dx, pt.y));
   1.126 +	float dfdy = get_height(Vector2(pt.x, pt.y + dy)) - get_height(Vector2(pt.x, pt.y - dy));
   1.127 +
   1.128 +	/* we need to multiply with the final scale factors while constructing the tangent and
   1.129 +	 * bitangent vectors, otherwise the "aspect" of the normals will be wrong while changing
   1.130 +	 * the two scale factors independently.
   1.131 +	 */
   1.132 +	Vector3 tang = Vector3(2.0 * dx * scale, dfdx * height_scale, 0);
   1.133 +	Vector3 bitan = Vector3(0, dfdy * height_scale, 2.0 * dy * scale);
   1.134 +	return cross_product(bitan, tang).normalized();
   1.135 +}
   1.136 +
   1.137 +float Terrain::get_world_height(const Vector2 &pt) const
   1.138 +{
   1.139 +	return get_height(world_to_uv(pt)) * height_scale;
   1.140 +}
   1.141 +
   1.142 +/* step over pixel distances until we cross the terrain, then determine the
   1.143 + * point of intersection
   1.144 + */
   1.145 +bool Terrain::intersect(const Ray &ray, float *dist) const
   1.146 +{
   1.147 +	// axis-aligned bounding box of the terrain
   1.148 +	AABox aabb;
   1.149 +	aabb.min = Vector3(-0.5 * scale, 0, -0.5 * scale);
   1.150 +	aabb.max = Vector3(0.5 * scale, height_scale, 0.5 * scale);
   1.151 +
   1.152 +	Ray mray;
   1.153 +	mray.origin = ray.origin;
   1.154 +	// find a reasonable step size
   1.155 +	float pixsz = dx > dy ? dx : dy;
   1.156 +	float raysz = ray.dir.length();
   1.157 +	mray.dir = ray.dir / raysz * pixsz;
   1.158 +
   1.159 +	float aabb_dist = 0.0;
   1.160 +
   1.161 +	if(!aabb.contains(mray.origin)) {
   1.162 +		/* if we're not in the aabb, calculate the intersection point of the ray
   1.163 +		 * with the aabb, to start ray-marching from that point.
   1.164 +		 */
   1.165 +		HitPoint hit;
   1.166 +		if(!aabb.intersect(ray, &hit)) {
   1.167 +			return false;	// the ray misses the terrain completely
   1.168 +		}
   1.169 +		aabb_dist = hit.dist;
   1.170 +		mray.origin += ray.dir * aabb_dist;
   1.171 +	}
   1.172 +
   1.173 +
   1.174 +	// ray-march over the terrain until we find an intersection or leave the box
   1.175 +	int num_steps = 0;
   1.176 +	do {
   1.177 +		float mdist;
   1.178 +		if(intersect_micro(mray, &mdist, 0.01)) {
   1.179 +			/* to calculate the distance travelled along the original ray we need to
   1.180 +			 * first add the distance to the aabb where we started ray marching. Then
   1.181 +			 * add the fraction of the last step to the number of micro-steps up to
   1.182 +			 * this point, which have to be converted to the original ray scale by
   1.183 +			 * multiplying by the magnitude of the micro-ray direction, over the
   1.184 +			 * magnitude of the original ray direction.
   1.185 +			 */
   1.186 +			*dist = aabb_dist + (mdist + (float)num_steps) * pixsz / raysz;
   1.187 +			return true;
   1.188 +		}
   1.189 +
   1.190 +		mray.origin += mray.dir;
   1.191 +		num_steps++;
   1.192 +	} while(aabb.contains(mray.origin));
   1.193 +
   1.194 +	return false;
   1.195 +}
   1.196 +
   1.197 +bool Terrain::intersect_micro(const Ray &ray, float *dist, float thres) const
   1.198 +{
   1.199 +	Vector3 start = ray.origin;
   1.200 +	Vector3 end = ray.origin + ray.dir;
   1.201 +
   1.202 +	float hstart = get_world_height(Vector2(start.x, start.z));
   1.203 +	float hend = get_world_height(Vector2(end.x, end.z));
   1.204 +
   1.205 +	/* if both the start and the end of the ray are above or below the heightfield
   1.206 +	 * then there's no possible intersection in micro-scales.
   1.207 +	 */
   1.208 +	if(start.y > hstart && end.y > hend) {
   1.209 +		return false;	// all above
   1.210 +	}
   1.211 +	if(start.y < hstart && end.y < hend) {
   1.212 +		return false;	// all below
   1.213 +	}
   1.214 +
   1.215 +	/* otherwise (one on one side and the other on the other side), we're straddling
   1.216 +	 * the heightfield. So, find the mid-point and binary-search until we find an
   1.217 +	 * estimated intersection point within the limits defined by "thres".
   1.218 +	 */
   1.219 +	Vector3 mid = lerp(start, end, 0.5);
   1.220 +	float hmid = get_world_height(Vector2(mid.x, mid.z));
   1.221 +	float dh = mid.y - hmid;
   1.222 +
   1.223 +	int iter = 0;	// iter is there to avoid infinite loops in marginal cases
   1.224 +	while(fabs(dh) > thres && iter < 100) {
   1.225 +		if(dh > 0) {	// mid is above the surface
   1.226 +			start = mid;
   1.227 +		} else {		// mid is below the surface
   1.228 +			end = mid;
   1.229 +		}
   1.230 +		mid = lerp(start, end, 0.5);
   1.231 +		hmid = get_world_height(Vector2(mid.x, mid.z));
   1.232 +		dh = mid.y - hmid;
   1.233 +
   1.234 +		iter++;
   1.235 +	}
   1.236 +
   1.237 +	// found the intersection point, calculate the parametric distance and return true
   1.238 +	*dist = (mid - ray.origin).length() / ray.dir.length();
   1.239 +	return true;
   1.240 +}
   1.241 +
   1.242 +void Terrain::draw(int usub, int vsub) const
   1.243 +{
   1.244 +	if(usub < 1) usub = xsz - 1;
   1.245 +	if(vsub < 1) vsub = ysz - 1;
   1.246 +
   1.247 +	if(dlist && dlist_sub[0] == usub && dlist_sub[1] == vsub) {
   1.248 +		glCallList(dlist);
   1.249 +		return;
   1.250 +	}
   1.251 +
   1.252 +	if(!dlist) {
   1.253 +		dlist = glGenLists(2);
   1.254 +		dlist_norm = dlist + 1;
   1.255 +	}
   1.256 +	glNewList(dlist, GL_COMPILE_AND_EXECUTE);
   1.257 +	dlist_sub[0] = usub;
   1.258 +	dlist_sub[1] = vsub;
   1.259 +
   1.260 +	float du = 1.0 / (float)usub;
   1.261 +	float dv = 1.0 / (float)vsub;
   1.262 +
   1.263 +	glBegin(GL_QUADS);
   1.264 +
   1.265 +	float v = 0.0;
   1.266 +	for(int i=0; i<vsub; i++) {
   1.267 +		float u = 0.0;
   1.268 +		for(int j=0; j<usub; j++) {
   1.269 +
   1.270 +			Vector2 uv(u, v);
   1.271 +			Vector3 norm = get_normal(uv);
   1.272 +			Vector2 wpt = uv_to_world(Vector2(u, v));
   1.273 +			glNormal3f(norm.x, norm.y, norm.z);
   1.274 +			glVertex3f(wpt.x, get_height(Vector2(u, v)) * height_scale, wpt.y);
   1.275 +
   1.276 +			uv = Vector2(u, v + dv);
   1.277 +			norm = get_normal(uv);
   1.278 +			wpt = uv_to_world(uv);
   1.279 +			glNormal3f(norm.x, norm.y, norm.z);
   1.280 +			glVertex3f(wpt.x, get_height(uv) * height_scale, wpt.y);
   1.281 +
   1.282 +			uv = Vector2(u + du, v + dv);
   1.283 +			norm = get_normal(uv);
   1.284 +			wpt = uv_to_world(uv);
   1.285 +			glNormal3f(norm.x, norm.y, norm.z);
   1.286 +			glVertex3f(wpt.x, get_height(uv) * height_scale, wpt.y);
   1.287 +
   1.288 +			uv = Vector2(u + du, v);
   1.289 +			norm = get_normal(uv);
   1.290 +			wpt = uv_to_world(uv);
   1.291 +			glNormal3f(norm.x, norm.y, norm.z);
   1.292 +			glVertex3f(wpt.x, get_height(uv) * height_scale, wpt.y);
   1.293 +
   1.294 +			u += du;
   1.295 +		}
   1.296 +		v += dv;
   1.297 +	}
   1.298 +	glEnd();
   1.299 +
   1.300 +	glEndList();
   1.301 +}
   1.302 +
   1.303 +void Terrain::draw_normals(float sz, int usub, int vsub) const
   1.304 +{
   1.305 +	if(usub < 1) usub = xsz - 1;
   1.306 +	if(vsub < 1) vsub = ysz - 1;
   1.307 +
   1.308 +	if(dlist_norm && dlist_norm_sub[0] == usub && dlist_norm_sub[1] == vsub && fabs(dlist_norm_sz - sz) < 0.0001) {
   1.309 +		glCallList(dlist_norm);
   1.310 +		return;
   1.311 +	}
   1.312 +
   1.313 +	if(!dlist_norm) {
   1.314 +		dlist = glGenLists(2);
   1.315 +		dlist_norm = dlist + 1;
   1.316 +	}
   1.317 +	glNewList(dlist_norm, GL_COMPILE_AND_EXECUTE);
   1.318 +	dlist_norm_sub[0] = usub;
   1.319 +	dlist_norm_sub[1] = vsub;
   1.320 +	dlist_norm_sz = sz;
   1.321 +
   1.322 +	float du = 1.0 / (float)usub;
   1.323 +	float dv = 1.0 / (float)vsub;
   1.324 +
   1.325 +	glBegin(GL_LINES);
   1.326 +
   1.327 +	float v = 0.0;
   1.328 +	for(int i=0; i<vsub + 1; i++) {
   1.329 +		float u = 0.0;
   1.330 +		for(int j=0; j<usub + 1; j++) {
   1.331 +			Vector2 uv(u, v);
   1.332 +			Vector2 wpt = uv_to_world(uv);
   1.333 +			Vector3 norm = get_normal(uv) * sz * height_scale;
   1.334 +
   1.335 +			Vector3 p(wpt.x, get_height(uv) * height_scale, wpt.y);
   1.336 +			glVertex3f(p.x, p.y, p.z);
   1.337 +			glVertex3f(p.x + norm.x, p.y + norm.y, p.z + norm.z);
   1.338 +
   1.339 +			u += du;
   1.340 +		}
   1.341 +		v += dv;
   1.342 +	}
   1.343 +	glEnd();
   1.344 +
   1.345 +	glEndList();
   1.346 +}
   1.347 +
   1.348 +void Terrain::invalidate_mesh()
   1.349 +{
   1.350 +	// this will force recreation of the display lists on the next draw call
   1.351 +	dlist_sub[0] = dlist_sub[1] = dlist_norm_sub[0] = dlist_norm_sub[1] = -1;
   1.352 +}