Texture splatting on a terrain
If you want to render a terrain, it's often needed to render different sections with different textures to create grass, rocks, roads etc. The transitions between these sections should be smooth in most cases. One solution for this is texture splatting. In this example code, some random terrain mesh created by terragen is used. It will be rendered by using three actual textures and one splatting/blending texture. You can find the sources and the assets in the zip file.
The splatting is done in a custom shader by taking the rgb values from the splatting texture and mixing the other textures according to their values. The sand texture is used twice in this shader to allow for more variety.
Here's the source code:
import org.lwjgl.opengl.Display; import com.threed.jpct.Camera; import com.threed.jpct.Config; import com.threed.jpct.FrameBuffer; import com.threed.jpct.GLSLShader; import com.threed.jpct.IRenderer; import com.threed.jpct.Loader; import com.threed.jpct.Logger; import com.threed.jpct.Matrix; import com.threed.jpct.Object3D; import com.threed.jpct.PolygonManager; import com.threed.jpct.SimpleVector; import com.threed.jpct.Texture; import com.threed.jpct.TextureInfo; import com.threed.jpct.TextureManager; import com.threed.jpct.World; import com.threed.jpct.util.Light; /** * This example shows basic texture splatting on a terrain by using a custom * shader. The mesh used has been generated by terragen and provides no proper * texture coodinates, which is why this example creates them in code. * * @author EgonOlsen */ public class TextureSplat { private FrameBuffer buffer; private World world; private Object3D terrain; private GLSLShader splatter; /** * @param args */ public static void main(String[] args) { new TextureSplat().haveFun(); } /** * Starts the application */ private void haveFun() { init(); render(); } /** * Loads the required files and sets up the rendering. It's all pretty basic * stuff in here except for the creation of the shader that does the actual * texture splatting.<br/> * Also note that the setTexture()-operation is actually done in an * additional method, because the mesh used has no proper coordinates. */ private void init() { Config.maxTextureLayers = 4; Logger.setOnError(Logger.ON_ERROR_THROW_EXCEPTION); buffer = new FrameBuffer(1024, 768, FrameBuffer.SAMPLINGMODE_HARDWARE_ONLY); buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE); buffer.enableRenderer(IRenderer.RENDERER_OPENGL); TextureManager tm = TextureManager.getInstance(); tm.addTexture("grass", new Texture("assets/grass.jpg")); tm.addTexture("sand", new Texture("assets/sand.jpg")); tm.addTexture("rocks", new Texture("assets/rocks.jpg")); tm.addTexture("splat", new Texture("assets/splat2.png")); terrain = Object3D.mergeAll(Loader.loadOBJ("assets/terrain.obj", "assets/terrain.mtl", 1)); terrain.rotateX(-(float) Math.PI / 2f); terrain.rotateMesh(); terrain.clearRotation(); setTexture(terrain); terrain.compile(); terrain.build(); splatter = new GLSLShader(Loader.loadTextFile("assets/splatter.vert"), Loader.loadTextFile("assets/splatter.frag")); splatter.setStaticUniform("map0", 0); splatter.setStaticUniform("map1", 1); splatter.setStaticUniform("map2", 2); splatter.setStaticUniform("map3", 3); terrain.setRenderHook(splatter); world = new World(); world.setClippingPlanes(1, 15000); world.addObject(terrain); world.setAmbientLight(0, 0, 0); Light light = new Light(world); light.setPosition(new SimpleVector(500, -4000, -2000)); light.setAttenuation(-1); light.setIntensity(200, 255, 255); Camera camera = world.getCamera(); camera.setPosition(0, -3500, -500); camera.lookAt(terrain.getTransformedCenter()); } /** * */ private void render() { Matrix splatMove=new Matrix(); while (!Display.isCloseRequested()) { // Move the splatting texture...just for fun... splatMove.translate(0.001f, 0, 0); splatter.setUniform("splatTransform", splatMove); buffer.clear(); world.renderScene(buffer); world.draw(buffer); // Show the actual splatting texture buffer.blit(TextureManager.getInstance().getTexture("splat"), 0, 0, 0, 0, 512, 512, 128, 128, -1, false); buffer.update(); buffer.displayGLOnly(); terrain.rotateY(0.005f); sleep(); } System.exit(0); } /** * This method generates texture coordinates for the mesh based on the * coordinates in object space. The normal texture layers are tiled while * the splatting texture (which is the last layer) covers the mesh exactly. * * @param obj */ private void setTexture(Object3D obj) { TextureManager tm = TextureManager.getInstance(); terrain.calcBoundingBox(); float[] bb = terrain.getMesh().getBoundingBox(); float minX = bb[0]; float maxX = bb[1]; float minZ = bb[4]; float maxZ = bb[5]; float dx = maxX - minX; float dz = maxZ - minZ; float dxs = dx; float dzs = dz; dx /= 200f; dz /= 200f; float dxd = dx; float dzd = dz; int tid = tm.getTextureID("grass"); int sid = tm.getTextureID("rocks"); int trid = tm.getTextureID("sand"); int bid = tm.getTextureID("splat"); PolygonManager pm = terrain.getPolygonManager(); for (int i = 0; i < pm.getMaxPolygonID(); i++) { SimpleVector v0 = pm.getTransformedVertex(i, 0); SimpleVector v1 = pm.getTransformedVertex(i, 1); SimpleVector v2 = pm.getTransformedVertex(i, 2); // Assign textures for the first three layers (the "normal" // textures)... TextureInfo ti = new TextureInfo(tid, v0.x / dx, v0.z / dz, v1.x / dx, v1.z / dz, v2.x / dx, v2.z / dz); ti.add(sid, v0.x / dxd, v0.z / dzd, v1.x / dxd, v1.z / dzd, v2.x / dxd, v2.z / dzd, TextureInfo.MODE_ADD); ti.add(trid, v0.x / dxd, v0.z / dzd, v1.x / dxd, v1.z / dzd, v2.x / dxd, v2.z / dzd, TextureInfo.MODE_ADD); // Assign the splatting texture... ti.add(bid, -(v0.x - minX) / dxs, (v0.z - minZ) / dzs, -(v1.x - minX) / dxs, (v1.z - minZ) / dzs, -(v2.x - minX) / dxs, (v2.z - minZ) / dzs, TextureInfo.MODE_ADD); pm.setPolygonTexture(i, ti); } } private void sleep() { try { Thread.sleep(10); } catch (Exception e) { // DC } } }
This is the fragment shader:
uniform sampler2D map0; uniform sampler2D map1; uniform sampler2D map2; uniform sampler2D map3; varying vec2 texCoord0; varying vec2 texCoord1; varying vec2 texCoord2; varying vec2 texCoord3; varying vec4 vertexColor; void main(void) { vec4 col0 = texture2D(map0, texCoord0); vec4 col1 = texture2D(map1, texCoord1); vec4 col2 = texture2D(map2, texCoord2); vec4 col3 = col2/2.5; vec4 blend = texture2D(map3, texCoord3); gl_FragColor = vertexColor * mix(mix(mix(col0, col1, blend.r), col2, blend.g), col3, blend.b); }
And this is the vertex shader:
varying vec2 texCoord0; varying vec2 texCoord1; varying vec2 texCoord2; varying vec2 texCoord3; varying vec4 vertexColor; uniform mat4 splatTransform; const vec4 WHITE = vec4(1.0,1.0,1.0,1.0); void main(void) { texCoord0=gl_MultiTexCoord0.xy; texCoord1=gl_MultiTexCoord1.xy*0.5; texCoord2=gl_MultiTexCoord2.xy; texCoord3=(splatTransform * vec4(gl_MultiTexCoord3.xy, 0.0, 1.0)).xy; vertexColor = (gl_FrontLightModelProduct.sceneColor * gl_FrontMaterial.ambient) + (gl_LightSource[0].ambient * gl_FrontMaterial.ambient); vec3 vVertex = vec3(gl_ModelViewMatrix * gl_Vertex); vec3 normalEye = normalize(gl_ModelViewMatrix * vec4(gl_Normal, 0.0)).xyz; float angle = dot(normalEye, normalize(gl_LightSource[0].position.xyz - vVertex.xyz)); if (angle > 0.0) { vertexColor += vec4(gl_LightSource[0].diffuse * angle + gl_LightSource[0].specular * pow(angle, gl_FrontMaterial.shininess)); } vertexColor=vec4(min(WHITE, vertexColor).xyz, 1.0); gl_Position = ftransform(); }
And this is the result: