The heightmap I want to use:
Scene without grass.jpg map :
Scene with grass.jpg map:
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();
My approach to displacements using height maps doesn't work. I want to generate a plane with a displaced surface, but the plane stays flat and white, or turns black.
I tried to change the path of the heightmap.jpg. I also tried different ThreeJS materials. I tried to use flatshading in the planeMesh.
In order to add the image as texture correctly you will need to import the image first
import image from "./j1wxR.jpg";
then you need to load the image with the texture loader :
// Loading the heightmap-texture
const loader = new THREE.TextureLoader();
const displacementMap = loader.load(image);
To be able to apply the texture on the material and to use "displacementMap", you will need to use both map
and displacementMap
(in your case you need to load the grass texture the same way - and add it to map)
// creating the plane with displacement
const planeMesh = new THREE.Mesh(
new THREE.PlaneGeometry(planeDim.x, planeDim.y, planeSeg.x, planeSeg.y),
new THREE.MeshPhongMaterial({
side: THREE.DoubleSide,
displacementMap: displacementMap,
map: displacementMap,
displacementScale: 5
})
);
Notice: I made the displacementScale
to be 5, just so you can see the difference.
here is a link to codesandbox with a live example: Codesandbox - example
Probably next you will need to figure out how to make the grid helper be on top of the mesh, and how to place the spheres (with y position) according to the mesh. (but for that, you will need to create another question).