/* Terrain - The terrain scene object. Handles terrain drawing, etc *//////////////////////////////////////////////////////////////////// #include #include "Engine/Scene/Scene.h" #include "Terrain.h" #include "textureBlender.h" // Macros ////////////////////////////////////////////////// #define CLAMP(a,b,c) (a < b ? b : (a > c ? c : a)) #define WRAP(x,y) ((unsigned)x % y) #define POINT(x, y) pointptr[x + (y * pointsize)] // Helper Functions ////////////////////////////////////////////////// //SmoothStep - Return a scalar of 0.0 to 1.0, based on the given value f32 SmoothStep (f32 val, f32 a, f32 b) { if (b == a) return 0.0f; val -= a; val /= (b - a); return CLAMP (val, 0.0f, 1.0f); } // Con/De-struction ////////////////////////////////////////////////// Terrain::Terrain(CScene *owner) { //Initialize stuff m_owner = owner; m_zonesize = 0; disp = 0; } Terrain::~Terrain() { Cleanup(); } //Cleanup - Frees all resources void Terrain::Cleanup() { //If we have terrain loaded.. if (m_zonesize) { //Free our video memory (*m_vaarea).reset(); //Free the buffers for each zone for (u32 x=0; x < ZONES; ++x) { delete [] m_zones[x].cells; delete [] m_zones[x].verts; delete m_zones[x].vertexBuf; } } } // Terrain functions ////////////////////////////////////////////////// //InitialBuild - Uses the map provided to calculate normals, layer data, etc void Terrain::InitialBuild(f32 high, f32 low) { // Normalize the elevations /////////////////////////////////////////////////////// #define SCALE(x,y) scale[x + (y * m_sizex)] f32 *scale = new f32[m_sizey * m_sizex]; celldata *pointptr = m_cells; u32 pointsize = m_sizex; //Copy them all to a temporary table for (u32 y = 0; y < m_sizey; ++y) for (u32 x = 0; x < m_sizex; ++x) SCALE(x,y) = (POINT(x, y).height - low) / (high - low); //Blend the values for (u32 x = 0; x < m_sizex; x++) { for (u32 y = 0; y < m_sizey; y++) { //Take samples from some surrounding elevations f32 smooth = 0.0f; u32 samples = 0; for (s32 xx = -BLEND_RANGE; xx <= BLEND_RANGE; xx++) for (s32 yy = -BLEND_RANGE; yy <= BLEND_RANGE; yy++) { smooth += SCALE(CLAMP((x + xx), 0, (m_sizex - 1)), CLAMP((y + yy), 0, (m_sizey - 1))); samples++; } //Insert the nice, smooth value POINT(x, y).height = (smooth / f32(samples)) * MAX_HEIGHT; } } // Calculate the layer data // - How much of each layer(sand,etc) the point contains /////////////////////////////////////////////////////// for (u32 y = 0; y < m_sizey; ++y) { for (u32 x = 0; x < m_sizex; ++x) { //Get the current cell celldata &c = POINT(x, y); ZeroMemory(c.layeralphas, sizeof(u8) * (LAYER_COUNT)); // Calculate the normal /////////////////////////////////////////////// //Find matching nearby points s32 top = WRAP((y - 1), m_sizey); s32 bottom = WRAP((y + 1), m_sizey); s32 left = WRAP((x - 1), m_sizex); s32 right = WRAP((x + 1), m_sizex); //Calculate the scaled normal (IS THIS ACTUALLY REQUIRED?) Vector3 normal, &cnorm = POINT(x, y).normal;; normal.x = (SCALE(left, y) * MAX_HEIGHT) - (SCALE(right, y) * MAX_HEIGHT); normal.y = 2.0f; normal.z = (SCALE(x, top) * MAX_HEIGHT) - (SCALE(x, bottom) * MAX_HEIGHT); normal = normal.direction(); //Calculate the normal used for rendering cnorm.x = POINT(left, y).height - POINT(right, y).height; cnorm.y = -2.0f; cnorm.z = POINT(x, top).height - POINT(x, bottom).height; cnorm = cnorm.direction(); // Determine the layer makeup of this cell /////////////////////////////////////////////// //Sand exists in the lowest parts of the terrain f32 sand = 0.0f;//SmoothStep(SCALE(x,y), 0.3f, 0.1f); //Low grass likes lowlands and flat areas f32 e = SmoothStep(normal.y, 0.75f, 1.0f); f32 lgrass = 1.0f;//SmoothStep(SCALE(x,y), 0.45f, 0.25f); lgrass = (e * lgrass) * 5.0f; lgrass = 0.0f;//CLAMP(lgrass, 0, 1); //Rock likes mild slopes and high elevations e = 0.0f;//SmoothStep(normal.y, 0.8f, 0.5f); e += SmoothStep(SCALE(x,y), 0.7f, 1.0f); f32 rock = CLAMP(e, 0, 1); //Dirt likes very steep slopes f32 dirt = 0.0f;//SmoothStep(normal.y, 0.7f, 0.4f); // Convert the obtained values into alphas for each texture /////////////////////////////////////////////// f32 total = sand + lgrass + rock + dirt; //Normal grass is what's left over c.layeralphas[LAYER_GRASS] = (1.0f - total) * 255; total = G3D::max(total, 1.0f); //Calculate the rest c.layeralphas[LAYER_LOWGRASS] = (lgrass / total) * 255; c.layeralphas[LAYER_DIRT] = (dirt / total) * 255; c.layeralphas[LAYER_ROCK] = (rock / total) * 255; c.layeralphas[LAYER_SAND] = (sand / total) * 255; } } /////////////////////////////////////////////////////// // Compile each zone into a vertex buffer /////////////////////////////////////////////////////// //Calculate the zone size m_zonearea = m_sizex / ZONE_GRID; m_zoneareasq = m_zonearea * m_zonearea; m_zonesize = m_zonearea + 1; m_zonesizesq = m_zonesize * m_zonesize; //Allocate an appropriate chunk of video memory u32 numsharedverticies = ZONES * ((m_zonesize + 1) * 2); u32 numverticies = (m_sizex * m_sizey) + numsharedverticies; m_vaarea = VARArea::create( numverticies * (sizeof(Vector2) + //Texture coordinates sizeof(Vector3) + //Normals sizeof(Vector3)) //Position + 8 ); //Padding //Walk the zones for (s32 y = 0; y < ZONE_GRID; ++y) { for (s32 x = 0; x < ZONE_GRID; ++x) { //Get the associated zone zone &z = m_zones[x + (y * ZONE_GRID)]; //For this zone, allocate a block of cells z.cells = (celldata*)malloc(sizeof(celldata) * m_zonesizesq); //Copy over all the cells a row at a time celldata *zonecells = &m_cells[(x * m_zonearea) + ((y * m_zonearea) * m_sizex)]; for (u32 i = 0; i < m_zonesize; ++i) memcpy( &z.cells[i * m_zonesize], &zonecells[i * m_sizex], sizeof(celldata) * m_zonesize ); //Compile the zone CompileZone(z, x + (y * ZONE_GRID)); } } //Get rid of temporary assets delete [] m_cells; delete [] scale; } //CompileZone - Compiles a zone into a renderable form void Terrain::CompileZone(zone &z, u32 zoneidx) { //Calculate zone offset u32 zone_ofs_x = (zoneidx % ZONE_GRID) * m_zonearea; u32 zone_ofs_y = (zoneidx / ZONE_GRID) * m_zonearea; //Allocate a block of memory for the verticies z.verts = (terrainvertex*)malloc(sizeof(terrainvertex) * m_zonesizesq); //Create the verticies based on our cell data for (u32 y = 0; y < m_zonesize; ++y) { for (u32 x = 0; x < m_zonesize; ++x) { //Convert into a vertex terrainvertex &vert = z.verts[x + (y * m_zonesize)]; vert.pos.x = (zone_ofs_x + x); vert.pos.y = z.cells[x + (y * m_zonesize)].height; vert.pos.z = (zone_ofs_y + y); //Bring over the normal data vert.normal = z.cells[x + (y * m_zonesize)].normal; //Set the texture coordinates vert.texcoord.x = (f32(x) / m_zonearea); vert.texcoord.y = 1.0f - (f32(y) / m_zonearea); } } //Construct the overlay texture for this zone textureBlender tB; Array alphaMaps[LAYER_COUNT]; //add all our source layer textures for (u32 i=0; i < LAYER_COUNT; ++i) { //Construct the alphamap alphaMaps[i].resize(m_zoneareasq); u8 *mapPtr = alphaMaps[i].getCArray(), *tmapPtr = mapPtr; //Convert the celldata into an alphamap for (u32 y=0; y < m_zonearea; ++y) for (u32 x=0; x < m_zonearea; ++x) *(tmapPtr++) = z.cells[x + (y * m_zonesize)].layeralphas[i]; tB.addTexture(i, m_layers[i], mapPtr); } //Blend everything to form a texture tB.updateOpacity(m_zonesize); z.zonetex = tB.blend(rd); //Stick all the vertex data into a video buffer z.vertexBuf = new VAR(z.verts, m_zonesizesq, m_vaarea); } // Generation functions ////////////////////////////////////////////////// //Generate_Circles - Creates some terrain using a circles algorithm bool Terrain::Generate_Circles( s32 nIterations, s32 sizex, s32 sizey, f32 maxdispl, f32 mindispl, s32 featuresize ) { //Clear our previous data Cleanup(); //#Cells = area + 1 (make zone division possible) m_sizex = sizex+1; m_sizey = sizey+1; //Allocate our cell buffer m_cells = new celldata[m_sizex * m_sizey]; celldata *pointptr = m_cells; u32 pointsize = m_sizex; //Initialize some variables srand(GetTickCount()); u32 halfX = sizex / 2; u32 halfZ = sizey / 2; //Initialize the cells for (u32 y = 0; y < m_sizey; ++y) { for (u32 x = 0; x < m_sizex; ++x) { //Start at a height of 0 POINT(x, y).height = x + y; POINT(x, y).shadow = false; } } //Start performing the iterations f32 high=m_sizey + m_sizex, low=0; /*for (s32 k = 0; k < nIterations; ++k) { //Pick a random location to create a circular displacement f32 disp; s32 x = ((f32)rand() / RAND_MAX) * (sizex - featuresize); x += featuresize / 2; s32 y = ((f32)rand() / RAND_MAX) * (sizey - featuresize); y += featuresize / 2; //Calculate a random displacement within limits disp = ((f64)rand() / (f64)RAND_MAX) * maxdispl; if (disp < mindispl) disp = mindispl; //Are we going to create a depression or an extrusion? s32 dispSign = (((f32)rand() / RAND_MAX) > 0.5f) ? 1 : -1; //Produce a random circle size s32 circleSize = (f32)(rand() % (featuresize - 10)) + 10.0f; for (s32 i = 0; i < sizey; ++i) { for (s32 j = 0; j < sizex; ++j) { //Obtain the distance of this grid coordinate from the circle f64 dist = (sqrt(f64( (i-x)*(i-x) + (j-y)*(j-y) )) * 2) / (f64)circleSize; //If the 'circle' is close enough... if (fabs(dist) <= 1) { //Add the deplacement for this cell celldata &c = POINT(j, i); //Add c.height += ((disp / 2) * dispSign) + cos(dist * G3D::pi()) * ((disp / 2) * dispSign); //Keep track of the highest/lowest points high = G3D::max(high, c.height); low = G3D::min(low, c.height); } } } }*/ //Build the map InitialBuild(high, low); return true; } //Load_Heightmap - Extrapolates terrain from a given colour heightmap bool Terrain::Load_Heightmap(const char *mapname) { // Attempt to open the heightmap /////////////////////////////////////////////////////// GImage heightmap; try { heightmap.load(mapname); } catch (const GImage::Error &err) { //Report it and give up LOG("Terrain") << "Attempt to load heightmap " << err.filename << " failed. (" << err.reason << ")"; return false; } //Perform some checks assert(heightmap.width < MAP_SIZE, "Map is too big!"); assert(heightmap.height < MAP_SIZE, "Map is too big!"); //Cleanup what we already have and continue Cleanup(); //Done! Start gathering info /////////////////////////////////////////////////////// f32 high=0, low=0; //Used to normalize the data later //#Cells = area + 1 (make zone division possible) m_sizex = heightmap.width + 1; m_sizey = heightmap.height + 1; //Allocate our cell buffer m_cells = new celldata[m_sizex * m_sizey]; celldata *pointptr = m_cells; u32 pointsize = m_sizex; //Extract the height values for (u32 y = 0; y < m_sizey; ++y) { for (u32 x = 0; x < m_sizex; ++x) { //Clamp the indices u32 xx = CLAMP (x, 1, (m_sizex - 2)); u32 yy = CLAMP (y, 1, (m_sizey - 2)); //Get our pixel Color3uint8 &pixel = heightmap.pixel3(xx, yy); //Determine the height of this point f32 e = (f32(pixel.r) / 30.0f) + (f32(pixel.g) / 48.0f); //Store the info in the cell POINT(xx, yy).height = e; POINT(xx, yy).shadow = false; //Keep track of the highest/lowest points high = G3D::max(high, POINT(xx, yy).height); low = G3D::min(low, POINT(xx, yy).height); } } //Build the map InitialBuild(high, low); return true; } // Drawing functions ////////////////////////////////////////////////// //renderZone - Draws a zone void Terrain::renderZone(u32 idx) { //Create our index list, depicting what to draw zone &z = m_zones[idx]; Array lIndex; //TODO: Make the index list static? It should never change anyway //Create the indexes for (u32 y=0 ; y < (m_zonesize - 1) ; ++y) { u32 rowStart = (y * m_zonesize); for (u32 x=0 ; x < (m_zonesize - 1); ++x) { //Add the appropriate indicies to the list lIndex.append(rowStart + (x + 0) + (1 * m_zonesize)); lIndex.append(rowStart + (x + 1) + (1 * m_zonesize)); lIndex.append(rowStart + (x + 1) + (0 * m_zonesize)); lIndex.append(rowStart + (x + 0) + (0 * m_zonesize)); } } //Setup our texture rd->setTexture(0, z.zonetex); //Render our pre-prepared vertex array rd->beginIndexedPrimitives(); { rd->setTexCoordArray(0, *(z.vertexBuf), 0, 2); rd->setNormalArray(*(z.vertexBuf), sizeof(Vector2)); rd->setVertexArray(*(z.vertexBuf), sizeof(Vector2) + sizeof(Vector3), 3); rd->sendIndices(RenderDevice::QUADS, lIndex); } rd->endIndexedPrimitives(); } //render - Draws the object the entity is representing void Terrain::render() { //Show our texture rd->pushState(); { rd->push2D(); rd->setTexture(0, m_zones[disp].zonetex); Draw::rect2D(Rect2D::xywh(0,0,512,512), rd); rd->pop2D(); //Render a zone for (u32 x=0; x < ZONES; ++x) { //Give each zone a nice colour //glColor3f((x % 2 == 0) ? 1.0f : 0, (x % 3 == 1) ? 1.0f : 0, (x % 4 == 2) ? 1.0f : 0); renderZone(x); } } rd->popState(); //Do we have anything to render? /*assert(m_bReady, "Terrain class used without being initialized."); // We need to calculate the coordinates that the camera's frustum // make with the terrain's maximally negatively displaced plane. /////////////////////////////////////////////////////// //Get the frustum and camera details CoordinateFrame coordf; Vector3 dir = -Vector3::unitY(); m_owner->m_overhead.getCoordinateFrame(coordf); //Create matricies to calculate the direction vectors emerging from the top left //and bottom right of the viewport Matrix3 matt1 = Matrix3::fromEulerAnglesXYZ( (-m_owner->m_overhead.getFieldOfView()) / 2.0f, 0, (-m_owner->m_overhead.getFieldOfView()) / 2.0f ); Matrix3 matt2 = Matrix3::fromEulerAnglesXYZ( (m_owner->m_overhead.getFieldOfView()) / 2.0f, 0, (m_owner->m_overhead.getFieldOfView()) / 2.0f ); //Create a Ray from the camera's position heading to the top left of the near plane Ray topLeft = Ray::fromOriginAndDirection(coordf.translation, dir * matt1); //..and a ray from the camera to the bottom right of the near plane Ray botRight = Ray::fromOriginAndDirection(coordf.translation, dir * matt2); //Create a plane to represent the terrain at maximum negative displacement Plane terrain( Vector3::unitY(), //UNIT_Y, since the normal is just vertically upwards Vector3(0, -m_minDisplacement, 0) ); //Since the plane is infinite, it can start at 0,0 //Obtain the point of intersection for both rays Vector3 tL = topLeft.intersection(terrain), bR = botRight.intersection(terrain); //Let it have some leeway tL.x += 20; bR.x -= 20; renderDevice->pushState(); //Setup the per-pixel shader renderDevice->setShader(m_shader); //Use the points of intersection to obtain the section of terrain to draw renderSection(renderDevice, (tL.x * (f32)SQUARE_SIZE), (tL.z * (f32)SQUARE_SIZE), (bR.x * (f32)SQUARE_SIZE), (bR.z * (f32)SQUARE_SIZE) ); //Restore shader renderDevice->setShader(NULL); renderDevice->popState();*/ } //update - Updates the state of the entity void Terrain::update() { } // Texturing functions ////////////////////////////////////////////////// //loadAssets - Loads assets related to terrain into memory (textures, etc.) void Terrain::loadAssets() { //Load the layer textures m_layers[LAYER_GRASS] = Texture::fromFile("images\\terrain\\grassa512.bmp"); m_layers[LAYER_LOWGRASS] = Texture::fromFile("images\\terrain\\grassb512.bmp"); m_layers[LAYER_SAND] = Texture::fromFile("images\\terrain\\sand512.bmp"); m_layers[LAYER_ROCK] = Texture::fromFile("images\\terrain\\rock512.bmp"); m_layers[LAYER_DIRT] = Texture::fromFile("images\\terrain\\dirt512.bmp"); //Allow the texture blender to load assets textureBlender::loadAssets(); } /* textureBlender - Used for blending textures together for the terrain *//////////////////////////////////////////////////////////////////// #include #include "textureBlender.h" #include "Terrain.h" // Static Initialization ////////////////////////////////////////////////// ShaderRef textureBlender::m_blendShader; //loadAssets - Loads all the external assets needed for this class void textureBlender::loadAssets() { //Load our blending shader m_blendShader = G3D::Shader::fromFiles("shaders/terrainblend.vert", "shaders/terrainblend.frag"); assert(m_blendShader->ok(), "Terrain texture blending shader failed to load."); } // Con/De-struction ////////////////////////////////////////////////// textureBlender::textureBlender() { } textureBlender::~textureBlender() { } // Texture functions ////////////////////////////////////////////////// //blend - Blends the given textures together using the specified alpha maps TextureRef textureBlender::blend(RenderDevice *rd, TextureRef oldSurface) { //Allocate a new texture to render to, if we don't have an old one to use TextureRef renderSurface; if (oldSurface.isNull()) renderSurface = Texture::createEmpty(TEXTURE_SIZE, TEXTURE_SIZE, "terrainTexture", TextureFormat::RGBA8, Texture::CLAMP, Texture::NO_INTERPOLATION, Texture::DIM_2D); else renderSurface = oldSurface; //Setup a framebuffer FramebufferRef fb = Framebuffer::create("terrainTexturer"); fb->set(Framebuffer::COLOR_ATTACHMENT0, renderSurface); //Set the rendering device up for rendering to our framebuffer/texture rd->pushState(); rd->push2D(fb); //First, clear the scene rd->setColorClearValue(Color3(0, 1, 0)); rd->clear(); //Render the opacity map stuff for(s32 i=0; i < m_sources.size(); i += 4) { //Break if there's nothing to draw if (m_sources[i + 0].source.isNull() && m_sources[i + 1].source.isNull() && m_sources[i + 2].source.isNull() && m_sources[i + 3].source.isNull() ) break; bool source2 = (m_sources.size() > i + 1); bool source3 = (m_sources.size() > i + 2); bool source4 = (m_sources.size() > i + 3); //Load up the blending shader rd->setShader(m_blendShader); //Maybe move this call outside? faster? m_blendShader->args.set("opacityMap", m_opacity[i / 4]); m_blendShader->args.set("source0", m_sources[i + 0].source); //Replace textures that don't exist with the first texture to keep glsl happy. //The opacity map should ignore them regardless. m_blendShader->args.set("source1", m_sources[i + (source2 ? 1:0)].source); m_blendShader->args.set("source2", m_sources[i + (source3 ? 2:0)].source); m_blendShader->args.set("source3", m_sources[i + (source4 ? 3:0)].source); //Render the texture Draw::rect2D(Rect2D::xywh(0,0,TEXTURE_SIZE,TEXTURE_SIZE), rd, Color3::white()); break; } //Reset the render mode rd->setPixelProgram(NULL); rd->pop2D(); rd->popState(); fb->set(Framebuffer::COLOR_ATTACHMENT0, NULL); // Return the surface debugAssertGLOk(); return renderSurface; } //addTexture - Adds a texture to be blended using the specified alpha map void textureBlender::addTexture(u32 materialIdx, TextureRef tex, u8 *alphas) { //Make a new sourcetexture and jam it into the array SourceTexture s; s.alpha = alphas; s.source = tex; //Resize the array if necessary if (m_sources.size() <= (s32)materialIdx) m_sources.resize(materialIdx + 1); m_sources[materialIdx] = s; } //updateOpacity - Merges all the alpha maps into a single texture void textureBlender::updateOpacity(u32 alphamapsize) { //Ready the opacity texture array u32 alphamapsizesq = (alphamapsize*alphamapsize); m_opacity.clear(); m_opacity.resize((m_sources.size() / 4) + 1); //4 Textures for each opacity map //Build new maps for(s32 rangeStart=0; rangeStart < m_sources.size(); rangeStart +=4) { //If we have a null on source #rangeStart, we are done. if (!m_sources[rangeStart + 0].alpha) break; bool source2 = (m_sources.size() > rangeStart + 1); bool source3 = (m_sources.size() > rangeStart + 2); bool source4 = (m_sources.size() > rangeStart + 3); //Ready an image GImage tempMap(alphamapsize, alphamapsize, 4); u8 *tmpBits = tempMap.byte(); ZeroMemory(tmpBits, sizeof(u32) * alphamapsizesq); // Copy everything relevant into the texture... for(u32 x=0; x < alphamapsizesq; ++x) { //Always assume that the first alpha exists, but don't rely on the others tmpBits[0] = m_sources[rangeStart + 0].alpha[x]; if (source2) tmpBits[1] = m_sources[rangeStart + 1].alpha[x]; if (source3) tmpBits[2] = m_sources[rangeStart + 2].alpha[x]; if (source4) tmpBits[3] = m_sources[rangeStart + 3].alpha[x]; // Move to next chunk of data... tmpBits += 4; } // Make a texture and store it... m_opacity[rangeStart / 4] = Texture::fromGImage("terrainAlphaMap", tempMap); //NOTE: Does having a constant name for all alpha textures mean that // it'll just always default for the first one commited? } }