The heightmap I have selected:
Scene without grass.jpg map :
Scene with grass.jpg map:
https://i.sstatic.net/q6ScO.png
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
import * as dat from 'dat.gui';
// dimensions of the plane x, y
const planeDim = new THREE.Vector2(20, 20);
const planeSeg = new THREE.Vector2(100, 100);
// Cam start coordinates x, y, z
let camPos = new THREE.Vector3(-10, 30, 30);
// Cam settings
const camFOV = 45;
const camAspect = window.innerWidth / window.innerHeight;
let camNear = 0.1;
let camFar = 1000;
// AxesHelper size
let axesHelperSize = 5;
// Gridhelper dimensions x, y
const gridHelperDim = new THREE.Vector2(20, 20);
// Mouseover highligthed tile starting coordinates x, y, z
let tilePos = new THREE.Vector3(0.5, 0, 0.5);
// Mouseover highlighted tile dimensions x, y
const tileDim = new THREE.Vector2(1, 1);
// Creating variables to work with raycasting from mouseposition
const mousePosition = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
let intersects;
// Array of all sphere objects placed on the plane
const objects = [];
//creating the renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// enable shadows in the scene
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Creating the Scene
const scene = new THREE.Scene();
// Creating the camera
const camera = new THREE.PerspectiveCamera(
camFOV,
camAspect,
camNear,
camFar
);
// Declaring the Camera as an OrbitCamera
const orbit = new OrbitControls(camera, renderer.domElement);
camera.position.set(camPos.x, camPos.y, camPos.z);
orbit.update();
// creating and adding the AxesHelper to the scene
const axesHelper = new THREE.AxesHelper(axesHelperSize);
scene.add(axesHelper);
// Loading the heightmap-texture
const loader = new THREE.TextureLoader();
const displacementMap = loader.load('res/heightmap.jpg');
const map = loader.load('res/grass.jpg');
// creating the plane with displacement
const planeMesh = new THREE.Mesh(
new THREE.PlaneGeometry(planeDim.x, planeDim.y, planeSeg.x, planeSeg.y),
new THREE.MeshPhongMaterial({
color: 0xFFFFFF,
side: THREE.DoubleSide,
visible: true,
displacementMap: displacementMap,
displacementScale: 20,
map: map,
flatShading: false
})
);
// enable recieving shadows on the plane
planeMesh.receiveShadow = true;
// giving the plane a name
planeMesh.name = 'ground';
// adding the plane to the scene
scene.add(planeMesh);
// rotate the plane 90 degrees
planeMesh.rotation.x = -Math.PI / 2;
// creating the gridHelper on the plane
const gridHelper = new THREE.GridHelper(gridHelperDim.x, gridHelperDim.y);
// adding the gridhelper into the scene
scene.add(gridHelper);
// creating the highlighted tile, setting its position and adding it to the scene
const highlightMesh = new THREE.Mesh(
new THREE.PlaneGeometry(tileDim.x, tileDim.y),
new THREE.MeshBasicMaterial({
color: 0x00FF00,
side: THREE.DoubleSide,
transparent: true
})
);
highlightMesh.position.set(tilePos.x, tilePos.y, tilePos.z);
highlightMesh.rotation.x = -Math.PI / 2;
scene.add(highlightMesh);
// raycasting function. Tile on mouseposition will be highlighted
window.addEventListener('mousemove', function(e){
mousePosition.x = (e.clientX / this.window.innerWidth) * 2 - 1;
mousePosition.y = -(e.clientY / this.window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mousePosition, camera);
intersects = raycaster.intersectObjects(scene.children);
intersects.forEach(function(intersect){
if(intersect.object.name === 'ground'){
const highlightPos = new THREE.Vector3().copy(intersect.point).floor().addScalar(0.5);
highlightMesh.position.set(highlightPos.x, 0, highlightPos.z);
// returns true if tilespace is already used
const objectExists = objects.find(function(object){
return (object.position.x === highlightMesh.position.x)
&& (object.position.z === highlightMesh.position.z);
});
// changes tile color to white if tile is empty and red if not
if(!objectExists){
highlightMesh.material.color.setHex(0x00FF00);
}else{
highlightMesh.material.color.setHex(0xFF0000);
}
}
});
});
// Creating the sphere object
const sphereMesh = new THREE.Mesh(
new THREE.SphereGeometry(0.4, 4, 2),
new THREE.MeshStandardMaterial({
wireframe: true,
color: 0xAEAEDB
})
);
// enabling sphere to cast a shadow
sphereMesh.castShadow = true;
// Click event, clicking tile will spawn the sphere object on it
window.addEventListener('mousedown', function(){
// returns true if the clicked tile already has a sphere
const objectExists = objects.find(function(object){
return (object.position.x === highlightMesh.position.x)
&& (object.position.z === highlightMesh.position.z);
})
// if tile is empty, spawn a shpere, else - console log
if(!objectExists){
intersects.forEach(function(intersect){
if(intersect.object.name === 'ground'){
const sphereClone = sphereMesh.clone();
sphereClone.position.copy(highlightMesh.position);
scene.add(sphereClone);
objects.push(sphereClone);
//make tile red instantly after clicking to indicate the tile space is already in use
highlightMesh.material.color.setHex(0xFF0000);
}
})
}else{
console.log('Can not place, space is already used!')
}
});
// adding ambient light to the scene
const ambientLight = new THREE.AmbientLight(0x333333);
scene.add(ambientLight);
// adding a spotlight to the scene
const spotLight = new THREE.SpotLight(0xFFFFFF);
scene.add(spotLight);
spotLight.position.set(-100, 100, 0);
spotLight.castShadow = true;
spotLight.angle = 0.2;
// ading a lighthelper to help see the light settings
const sLightHelper = new THREE.SpotLightHelper(spotLight);
scene.add(sLightHelper);
// creating the light gui itself
const gui = new dat.GUI();
// adding options to dat.gui to change seetings of the light and plane
const options = {
angle: 0.2,
penumbra: 0,
intensity: 1,
wireframe: false
};
// creating the light gui settings and setting its boundries
gui.add(options, 'angle', 0, 1);
gui.add(options, 'penumbra', 0, 1);
gui.add(options, 'intensity', 0, 1);
// enables wireframemode for the plane
gui.add(options, 'wireframe').onChange(function(e){
planeMesh.material.wireframe = e;
});
// animation loop with time parameter
function animate(time){
// bind the gui options to the spotlight
spotLight.angle = options.angle;
spotLight.penumbra = options.penumbra;
spotLight.intensity = options.intensity;
// bind the gui options to the plane
planeMesh.wireframe = options.wireframe;
// update lighthelper appearence according to the settings
sLightHelper.update();
// make the tile blinking
highlightMesh.material.opacity = 1 + Math.sin(time / 120);
// rotation animation on every sphere object on the plane
objects.forEach(function(object){
object.rotation.x = time / 500;
object.rotation.y = time / 500;
object.position.y = 0.5 + 0.5 * Math.abs(Math.sin(time / 1000));
})
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
// logging in the browser console
console.log();
I am facing difficulties with my approach to using displacement maps for generating a plane with a displaced surface. Despite trying different textures and materials in ThreeJS, the plane remains either flat and white or turns black.
I attempted changing the path to the heightmap.jpg file and experimenting with various ThreeJS materials, including flat shading on the planeMesh.