Hi does anyone know how to add textures on 3d height map extension.
sorry bag example but
This is an example of a height map that I created using that extension and the problem is. How do I add a texture to it? Does anyone know how?
Hi does anyone know how to add textures on 3d height map extension.
I’m very late to this but I managed to this for my own project. Are you still active and need help?
Hi, thank you for answering my problem, even though I no longer continue using the 3D height map extension, I am curious about how you did it.
So for it to work you have to modify the extension it self but its easy. Since I don’t know Javascript I used Claude to modify the DefineHelperClasses function’s Javascript code.

if (gdjs.__heightMap3DExtension) {
return;
}
const defaultWidth = 64;
const defaultHeight = 64;
const defaultDepth = 64;
class HeightMap {
/** @type {gdjs.CustomRuntimeObject3D} */
object;
/** @type {THREE.Mesh} */
mesh;
/** @type {THREE.TypedArray} */
vertices;
/** @type {integer} */
dimX;
/** @type {integer} */
dimY;
/** @type {string} */
texturePath;
/** @type {THREE.Texture} */
texture;
hasChangedThisFrame = false;
isDirty = false;
/**
* @param object {gdjs.CustomRuntimeObject}
* @param dimX {integer}
* @param dimY {integer}
* @param texturePath {string}
* @param wireframe {boolean}
*/
constructor(object, dimX, dimY, texturePath, wireframe) {
this.object = object;
this.dimX = dimX;
this.dimY = dimY;
this.texturePath = texturePath;
this.rebuildPlane();
this.loadTexture(texturePath);
this.setWireframe(wireframe);
}
/**
* @param dimX {integer}
* @param dimY {integer}
*/
clearAndResize(dimX, dimY) {
this.dimX = dimX;
this.dimY = dimY;
this.rebuildPlane();
}
rebuildPlane() {
const material = this.mesh ? this.mesh.material : new THREE.MeshStandardMaterial({
roughness: 0.9,
metalness: 0.0,
envMapIntensity: 0.5
});
if (this.mesh) {
this.mesh.removeFromParent();
}
const geometry = new THREE.PlaneGeometry(64, 64, this.dimX - 1, this.dimY - 1);
this.vertices = geometry.attributes.position.array;
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.rotation.order = 'ZYX';
this.mesh.position.x = defaultWidth / 2;
this.mesh.position.y = defaultHeight / 2;
this.mesh.scale.y = -1;
this.object.get3DRendererObject().add(this.mesh);
this.hasChangedThisFrame = true;
}
forceUpdate() {
if (this.isDirty) {
this.isDirty = false;
this.mesh.geometry.attributes.position.needsUpdate = true;
this.mesh.geometry.computeVertexNormals();
}
}
updateIfNeeded() {
if (this.isDirty && !this.object.isHidden()) {
this.isDirty = false;
this.mesh.geometry.attributes.position.needsUpdate = true;
this.mesh.geometry.computeVertexNormals();
}
this.hasChangedThisFrame = false;
}
setAsDirty() {
this.hasChangedThisFrame = true;
this.isDirty = true;
}
/**
* @param indexX {integer}
* @param indexY {integer}
* @param value {float} a value between 0 and 1
*/
setGridValue(indexX, indexY, value) {
this.vertices[this.getVerticeZIndex(indexX, indexY)] =
defaultDepth * gdjs.evtTools.common.clamp(0, 1, value);
this.setAsDirty();
}
/**
* @param indexX {integer}
* @param indexY {integer}
* @returns {float} a value between 0 and 1
*/
getGridValue(indexX, indexY) {
return this.vertices[this.getVerticeZIndex(indexX, indexY)] / defaultDepth;
}
/**
* @param indexX {integer}
* @param indexY {integer}
* @returns {integer}
*/
getVerticeZIndex(indexX, indexY) {
return 2 + 3 * (indexX + indexY * this.dimX);
}
/**
* @param pointX {float} position in the scene
* @param pointY {float} position in the scene
* @return {float} the Z coordinate of the field point
*/
getFieldZ(pointX, pointY) {
const objectX = this.object.getX();
const objectY = this.object.getY();
const objectWidth = this.object.getWidth();
const objectHeight = this.object.getHeight();
if (pointX < objectX || pointX > objectX + objectWidth ||
pointY < objectY || pointY > objectY + objectHeight) {
return 0;
}
const x = (this.dimX - 1) * (pointX - objectX) / objectWidth;
const y = (this.dimY - 1) * (pointY - objectY) / objectHeight;
const integerPartX = Math.floor(x);
const integerPartY = Math.floor(y);
const floatPartX = x - integerPartX;
const floatPartY = y - integerPartY;
const bottomLeft = this.getGridValue(integerPartX, integerPartY + 1);
const topRight = this.getGridValue(integerPartX + 1, integerPartY);
let fieldValue = 0;
if (floatPartX + floatPartY > 1) {
// Bottom right triangle
const bottomRight = this.getGridValue(integerPartX + 1, integerPartY + 1);
fieldValue = (floatPartX + floatPartY - 1) * bottomRight +
(1 - floatPartX) * bottomLeft + (1 - floatPartY) * topRight;
}
else {
// Top left triangle
const topLeft = this.getGridValue(integerPartX, integerPartY);
fieldValue = (1 - floatPartX - floatPartY) * topLeft +
floatPartX * topRight + floatPartY * bottomLeft;
}
return this.object.getZ() + fieldValue * this.object.getDepth();
}
/**
* @param texture {PIXI.Texture}
* @param offsetX {integer}
* @param offsetY {integer}
*/
loadHeightMapFromTexture(texture, offsetX, offsetY) {
const imageData = getImageData(texture, offsetX, offsetY, this.dimX, this.dimY);
const size = this.dimX * this.dimY;
const colorRange = 0xffff;
const colorScale = defaultDepth / colorRange;
for (
let verticeIndex = 0, colorIndex = 0;
verticeIndex < 3 * size;
verticeIndex += 3, colorIndex += 4) {
// Read red and green channel to workaround canvas being 8 bits.
// Users need to convert their 16 bits grayscale images into 8 bits RGB.
const color = 256 * imageData.data[colorIndex] + imageData.data[colorIndex + 1];
this.vertices[verticeIndex + 2] = color * colorScale;
}
this.setAsDirty(true);
}
/**
* @param resourceName {string}
*/
loadTexture(resourceName) {
if (!resourceName || resourceName === '') {
// If no texture path, use a default white color
/** @type {THREE.MeshStandardMaterial} */
const material = this.mesh.material;
material.color.set(0xffffff);
material.map = null;
material.needsUpdate = true;
return;
}
// Get the runtime scene to access resources
const runtimeScene = this.object.getInstanceContainer();
try {
// Get the PIXI texture from GDevelop's image manager
const imageManager = runtimeScene.getGame().getImageManager();
const pixiTexture = imageManager.getPIXITexture(resourceName);
if (!pixiTexture || !pixiTexture.baseTexture || !pixiTexture.baseTexture.resource) {
console.error('Could not load PIXI texture for:', resourceName);
return;
}
// Get the URL from the PIXI texture
const textureUrl = pixiTexture.baseTexture.resource.url;
console.log('Loading texture from URL:', textureUrl);
const loader = new THREE.TextureLoader();
loader.load(
textureUrl,
(texture) => {
// Texture loaded successfully
console.log('Texture loaded successfully!');
this.texture = texture;
// Set texture to repeat
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// Set how many times to repeat (adjust these values as needed)
// Higher numbers = more repeats = smaller texture appearance
texture.repeat.set(24, 24);
// Set texture encoding to match GDevelop's rendering
texture.encoding = THREE.sRGBEncoding;
/** @type {THREE.MeshStandardMaterial} */
const material = this.mesh.material;
material.map = texture;
material.color.set(0xffffff); // Reset to white
material.needsUpdate = true;
},
undefined,
(error) => {
console.error('Error loading texture:', error);
console.error('Texture URL:', textureUrl);
}
);
} catch (error) {
console.error('Error accessing texture resource:', error);
console.error('Resource name:', resourceName);
}
}
/**
* @param resourceName {string}
*/
setColor(resourceName) {
// Renamed from setColor but keeping the same function name for compatibility
// Now it loads a texture instead
this.loadTexture(resourceName);
}
/**
* @param wireframe {boolean}
*/
setWireframe(wireframe) {
/** @type {THREE.MeshStandardMaterial} */
const material = this.mesh.material;
material.wireframe = wireframe;
}
}
/**
* @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}
*/
class PhysicsHeightMap {
/** @type {gdjs.Physics3DRuntimeBehavior} */
physics;
/** @type {HeightMap} */
heightMap;
/**
* @param physics {gdjs.Physics3DRuntimeBehavior}
* @param heightMap {HeightMap}
*/
constructor(physics, heightMap) {
this.physics = physics;
this.heightMap = heightMap;
this.physicsBodyUpdater = physics.bodyUpdater;
physics.bodyUpdater = this;
physics.recreateBody();
}
updateIfNeeded() {
if (this.heightMap.hasChangedThisFrame) {
this.physics.recreateBody();
}
}
/**
* @returns {Jolt.Body | null}
*/
createAndAddBody() {
const { physics } = this;
const { _sharedData } = physics;
const { dimX, dimY, vertices, object } = this.heightMap;
const shapeSettings = new Jolt.HeightFieldShapeSettings();
const width = object.getWidth() * _sharedData.worldInvScale;
const height = object.getHeight() * _sharedData.worldInvScale;
const depth = object.getDepth() * _sharedData.worldInvScale;
shapeSettings.mOffset.Set(-width / 2, -depth / 2, -height / 2);
shapeSettings.mScale.Set(
width / (dimX - 1),
object.getDepth() / defaultDepth,
height / (dimY - 1),
);
shapeSettings.mSampleCount = dimX;
const totalSize = dimX * dimY;
shapeSettings.mHeightSamples.resize(totalSize);
const heightSamples = new Float32Array(
Jolt.HEAPF32.buffer,
Jolt.getPointer(shapeSettings.mHeightSamples.data()),
totalSize);
for (let index = 0; index < heightSamples.length; index++) {
heightSamples[index] = vertices[2 + 3 * index] * _sharedData.worldInvScale;
// TODO Handle holes
// Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;
}
const shape = shapeSettings.Create().Get();
Jolt.destroy(shapeSettings);
const scaledShape = shape.ScaleShape(_sharedData.getVec3(1, 1, -1)).Get();
const position = physics._getPhysicsPosition(_sharedData.getRVec3(0, 0, 0));
const rotation = this._getPhysicsRotation(_sharedData.getQuat(0, 0, 0, 1));
const creationSettings = new Jolt.BodyCreationSettings(
scaledShape, position, rotation, Jolt.EMotionType_Static, physics.getBodyLayer());
const body = _sharedData.bodyInterface.CreateBody(creationSettings);
Jolt.destroy(creationSettings);
_sharedData.bodyInterface.AddBody(
body.GetID(),
Jolt.EActivation_Activate
);
return body;
}
/**
* @param result {Jolt.Quat}
*/
_getPhysicsRotation(result) {
const threeObject = this.heightMap.object.get3DRendererObject();
// TODO Use a static variable for the quaternion.
// It's not great to use the object rotation directly.
threeObject.rotation.x += Math.PI / 2;
result.Set(
threeObject.quaternion.x,
threeObject.quaternion.y,
threeObject.quaternion.z,
threeObject.quaternion.w
);
threeObject.rotation.x -= Math.PI / 2;
return result;
}
updateObjectFromBody() {
const { physics } = this;
const { _body } = physics;
// Copy transform from body to the GD object.
// The body is null when the behavior was either deactivated or the object deleted.
// It would be useless to try to recreate it as updateBodyFromObject already does it.
// If the body is null, we just don't do anything
// (but still run the physics simulation - this is independent).
if (_body !== null && _body.IsActive()) {
physics._moveObjectToPhysicsPosition(_body.GetPosition());
this._moveObjectToPhysicsRotation(_body.GetRotation());
}
}
/**
* @param physicsRotation {Jolt.Quad}
*/
_moveObjectToPhysicsRotation(physicsRotation) {
const { object } = this.heightMap;
const threeObject = object.get3DRendererObject();
threeObject.quaternion.x = physicsRotation.GetX();
threeObject.quaternion.y = physicsRotation.GetY();
threeObject.quaternion.z = physicsRotation.GetZ();
threeObject.quaternion.w = physicsRotation.GetW();
// TODO Avoid this instantiation
const euler = new THREE.Euler(0, 0, 0, 'ZYX');
euler.setFromQuaternion(threeObject.quaternion);
object.setRotationX(gdjs.toDegrees(euler.x) - 90);
object.setRotationY(gdjs.toDegrees(euler.y));
object.setAngle(gdjs.toDegrees(euler.z));
}
updateBodyFromObject() {
const { object } = this.heightMap;
const { physics } = this;
const { _sharedData } = physics;
if (physics._body === null) {
if (!physics._createBody()) return;
}
const body = physics._body;
if (
physics._objectOldX !== object.getX() ||
physics._objectOldY !== object.getY() ||
physics._objectOldZ !== object.getZ() ||
physics._objectOldRotationX !== object.getRotationX() ||
physics._objectOldRotationY !== object.getRotationY() ||
physics._objectOldRotationZ !== object.getAngle()
) {
_sharedData.bodyInterface.SetPositionAndRotationWhenChanged(
body.GetID(),
physics._getPhysicsPosition(_sharedData.getRVec3(0, 0, 0)),
this._getPhysicsRotation(_sharedData.getQuat(0, 0, 0, 1)),
Jolt.EActivation_Activate
);
}
}
recreateShape() {
this.physicsBodyUpdater.recreateShape();
}
destroyBody() {
this.physicsBodyUpdater.destroyBody();
}
}
/**
* @param texture {PIXI.Texture}
* @param offsetX {integer}
* @param offsetY {integer}
* @param width {integer}
* @param height {integer}
*/
function getImageData(texture, offsetX, offsetY, width, height) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
/** @type {PIXI.Rectangle} */
const crop = texture._frame.clone();
crop.x += offsetX;
crop.y += offsetY;
crop.width = Math.min(crop.width, width);
crop.height = Math.min(crop.height, height);
// TODO Should the resolution be taken into account?
// /** @type {number} */
// const resolution = texture.baseTexture.resolution;
// crop.x *= resolution;
// crop.y *= resolution;
// crop.width *= resolution;
// crop.height *= resolution;
canvas.width = width;
canvas.height = height;
context.save();
context.globalCompositeOperation = 'copy';
context.drawImage(
texture.baseTexture.getDrawableSource(),
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
crop.width,
crop.height
);
context.restore();
return context.getImageData(0, 0, width, height);
}
gdjs.__heightMap3DExtension = {
HeightMap,
PhysicsHeightMap,
};
You also need to modify HeightMap objects properties so it want’s a texture instead of a color.
texture.repeat.set(24, 24);
I have it set to repeat 24x24.
Usually, 3D models are used for the visual aspect and the height map for collisions.