Question about Terrain Shading

Hello.

I apologize in advance if this is an inane question; in truth I do not have a great deal of background in 3D graphics, though I have been attempting to remedy that.

I am currently in the process of trying to develop a flight simulator, primarily for my own use and have decided to use JMonkeyEngine 3 for rendering the external world. I have spent much of the last week working out a rather nice system of loading DEM data from multiple sources and interpolating from it onto a fixed grid to generate a “heightmap” float array to use with a TerrainQuad.

However, there is something which I do not understand about terrain shading.

Since I am just doing some prototyping at the moment and have not worked out a proper texture-generation system, I am just using the TerrainLighting material with one DiffuseMap (using the sample file “Textures/Terrain/splat/dirt.jpg” as the texture), one NormalMap (also using the sample file “Textures/Terrain/splat/dirt_normal.png”) and an AlphaMap consisting of a purely red image (since there is only one DiffuseMap). I have added a single white directional light with direction (-1f, -1f, 0), which is approximately pointing roughly downward (negative y) and towards the south (negative x ).

Nonetheless, whereas the mountains of the terrain are clearly visible from the north, due to the shading, they nearly cannot be made out at all from the south. Below are some screenshots from my test (the column marks the origin).

From the north (good)

I am at a loss as to what is going on. I have tried changing the directional light’s direction and tried using the default grass texture (and normal map). Just to be clear, there is no shadow rendering or filtering being used. No matter what, it seems like the terrain is always more clearly defined (i.e. has more darker “shaded” regions) on the northern side, even when the directional light source is pointing downward/southwards (i.e. from the north).

I would appreciate any ideas as to what might be happening here. Here is all of the code pertinent to JME3 (it references other classes I have written)

/*
 * Some notes regarding the JME3 coordinate system:
 * x: north(+)/south(-)
 * y: up(+)/down(-)
 * z: east(+)/west(-)
 */
package run;

import java.util.logging.*;
import java.util.ArrayList;
import java.util.List;
import java.lang.Math;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.scene.*;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.*;
import com.jme3.terrain.heightmap.*;
import com.jme3.terrain.geomipmap.lodcalc.*;
import com.jme3.texture.*;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.renderer.Camera;
import com.jme3.light.*;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.shadow.*;

import input.Input;
import io.FileUtils;
import terrain.DigitalElevationModelData;
import terrain.TerrainManager;
import math.*;

/**
 * Contains a test to load DEM data and render in on a TerrainQuad
 */
public class TerrainTest extends SimpleApplication{
	
	public final TerrainManager terrainManager = new TerrainManager();
	
	public static void run(){
		System.out.println("Starting Terrain Test");
		TerrainTest test = new TerrainTest();
		
		//DigitalElevationModelData.test();
		//ProjectionStereographic.test();
		//TerrainManager.test();
		
		test.start();
	}
	
	
	//for use elsewhere
	public Node getRootNode(){
		return rootNode;
	}
	
	@Override
	public void simpleInitApp(){
		//disable annoying logging
		Logger.getLogger("com.jme3").setLevel(Level.OFF);
		
		//Temporary: find a more permanent solution later:
		assetManager.registerLocator(FileUtils.ASSET_PATH, FileLocator.class);
		
		
		//setup input
		inputManager.deleteMapping("FLYCAM_ZoomIn");
		inputManager.deleteMapping("FLYCAM_ZoomOut");
		Input.INSTANCE.bind(inputManager);
		
		float moveSpeed = 500f;
		flyCam.setMoveSpeed(moveSpeed);
		flyCam.setZoomSpeed(0);
		
		Camera cam = getCamera();
		cam.setFrustumFar(500f*1000f);
		cam.setLocation(new Vector3f(0f, 1000f, 500f));
		
		viewPort.setBackgroundColor(new ColorRGBA(240.0f/255, 1f, 1f, 1f));
		makeBox();
		makeClaremont();
		makeSun();
	}
	
	
	private void makeBox(){
		Box b = new Box(15, 5000, 50);
		Box b2 = new Box(50, 5000, 15);
		Geometry geom = new Geometry("Box", b);
		Geometry geom2 = new Geometry("Box2", b2);
		geom.move(0f, 5000f, 0f);
		geom2.move(-50f, 5000f, 0f);
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
		//mat.setColor("Color", ColorRGBA.Green);
		geom.setMaterial(mat);
		geom2.setMaterial(mat);
		Node n = new Node();
		n.attachChild(geom);
		n.attachChild(geom2);
		rootNode.attachChild(n);
		
		
	}
	
	
	private Material getTerrainMaterial(){
		Material mat;
		
		mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
		mat.setBoolean("useTriPlanarMapping", false);
		mat.setFloat("Shininess", 0.0f);
		
		
		Texture grass = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
		grass.setWrap(WrapMode.Repeat);
		
		Texture grassNormal = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png");
		grassNormal.setWrap(WrapMode.Repeat);
		
		//make a really big texture
		Image image = grass.getImage();
		
		Texture alpha = assetManager.loadTexture("Textures/red.png");
		mat.setTexture("AlphaMap", alpha);
		
		
		mat.setTexture("DiffuseMap", grass);
		//mat.setTexture("Alpha", grass);
		mat.setFloat("DiffuseMap_0_scale", 64f);
		
		mat.setTexture("NormalMap", grassNormal);
		
		
		//mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
		return mat;
	}
	
	
	
	private void makeClaremont(){
		
		//make projection
		double centerLat = 34.110524;
		double centerLon = -117.728189;
		Projection projection = new ProjectionStereographic(centerLat, centerLon);
		
		Node terrainNode = new Node();
		
		//just make a terrain quad
		Material mat = getTerrainMaterial();

		
		int patchDimensions = 4*512;
		int jme3num = 64;
		float patchWidth = 100*1000f; //width in meters
		float scaleFactor = patchWidth/patchDimensions;
		
		System.out.format("Resolution: %f meters\n", scaleFactor);
		System.out.format("Generating heightmap for %d points . . .\n", (patchDimensions+1) * (patchDimensions+1));
		
		//arrays
		float[] elevArray = new float[(patchDimensions+1) * (patchDimensions+1)];
		
		long startTime = System.currentTimeMillis();
		for(int i = 0; i <= patchDimensions; i++){
			for(int j = 0; j <= patchDimensions; j++){
				int index = i*(patchDimensions+1) + j;
				
				//first, get x and z values from j and i
				float xval = scaleFactor * (j - (0.5f * patchDimensions));
				float zval = scaleFactor * (i - (0.5f * patchDimensions));
				
				//convert to lat/lon
				//Note: x and z must be switched due to JME3's orientation
				CoordinateLatLon coordinate = projection.transformInverse(zval, xval);
				double lat = coordinate.lat;
				double lon = coordinate.lon;
				
				//finally, get elevation data
				elevArray[index] = terrainManager.getElevation(lat, lon);
				//System.out.format("%f %f : %f\n", lat, lon, elevArray[index]);
			}
		}
		long endTime = System.currentTimeMillis();
		System.out.format("Done.  Time taken: %f seconds\n", (endTime - startTime)/1000.0);
		
		TerrainQuad terrain = new TerrainQuad("sea", jme3num+1, patchDimensions + 1, elevArray);
		terrain.setMaterial(mat);
		
		terrain.setLocalScale(scaleFactor, 1*1f, scaleFactor);
		terrainNode.attachChild(terrain);
		
		rootNode.attachChild(terrainNode);
		
		
		//terrainNode.setShadowMode(ShadowMode.CastAndReceive);
		
	}
	
	
	
	private DirectionalLight sun;
	private double theta = 0; //sun angle
	
	private void makeSun(){
		//add a light
		sun = new DirectionalLight();
		sun.setColor(ColorRGBA.White.mult(0.7f));
		sun.setDirection(new Vector3f(-1f, -1.0f, 0).normalizeLocal());
		rootNode.addLight(sun);
		
		AmbientLight al = new AmbientLight();
		al.setColor(ColorRGBA.Blue.mult(2.0f));
		//rootNode.addLight(al);
		
		//shadow tests?
		/*rootNode.setShadowMode(ShadowMode.Off);
		int ms = 1048;
		DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, ms, 3);
		dlsr.setLight(sun);
		//viewPort.addProcessor(dlsr);*/
	}
	
	
	private void updateSun(){
		if(Input.INSTANCE.pressed("Space")){
			theta += 0.01;//0.1 * Math.PI;
		}
		
		sun.setDirection(new Vector3f(-1f, (float) -Math.cos(theta), (float) Math.sin(theta)).normalizeLocal());
	}
	
	
	
	@Override
	public void simpleUpdate(float timePerFrame){
		//Update input
		Input.INSTANCE.update();
		
		//update sun
		updateSun();
		
		//camera
		updateCamera(timePerFrame);
	}
	
	
	private void updateCamera(float timePerFrame){
		
		boolean printMoveSpeed = false;
		if(Input.INSTANCE.pressed("Add")||Input.INSTANCE.trigger("Wheel Up")){
			float moveSpeed = flyCam.getMoveSpeed();
			moveSpeed *= 1.1;
			flyCam.setMoveSpeed(moveSpeed);
			printMoveSpeed = true;
		}
		if(Input.INSTANCE.pressed("Subtract")||Input.INSTANCE.trigger("Wheel Down")){
			float moveSpeed = flyCam.getMoveSpeed();
			moveSpeed /= 1.1;
			if(moveSpeed < 0){
				moveSpeed = 0;
			}
			flyCam.setMoveSpeed(moveSpeed);
			printMoveSpeed = true;
		}
		
		if(printMoveSpeed && false){
			String out = "Camera moveSpeed = %f%n";
			System.out.format(out, flyCam.getMoveSpeed());
		}
	}
	
	
	
}

Thank you.

try (0,-1,0)
that will let the light shine directly from above.
try that first in order to evaluate the situation.

the (-1,-1,0) is 45° directly from the north - and you should do (-1,-1,0).normalizeLocal(
normalize means that it will create a vector of unit length.
Local means that it will operate on the vector itself.