Skip to content

Instantly share code, notes, and snippets.

@ertugrulcetin
Last active October 31, 2022 16:53
Show Gist options
  • Save ertugrulcetin/1402396c998e661c367d1f1a57bf7976 to your computer and use it in GitHub Desktop.
Save ertugrulcetin/1402396c998e661c367d1f1a57bf7976 to your computer and use it in GitHub Desktop.
Generate terrain from heightmap in the editor for PlayCanvas
// Download Violentmonkey -> https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag/related
// Paste the code for https://playcanvas.com
// Sample project: https://playcanvas.com/project/1002857/overview/render-heightmap-in-the-editor
// ==UserScript==
// @name PlayCanvas
// @namespace Violentmonkey Scripts
// @match https://playcanvas.com/editor/scene/*
// @grant none
// @version 1.0
// @author -
// @description 10/31/2022, 6:56:52 PM
// ==/UserScript==
// Generate terrain from heightmap in the editor for PlayCanvas
function run() {
var entity = pc.app.root.findByName("heightmap");
if (entity) {
pc.app.root.removeChild(entity);
}
var opts = editor.entities.root.findByName('Terrain').json().components.script.scripts.terrain.attributes;
var shaderOpts = editor.entities.root.findByName('Terrain').json().components.script.scripts.shaderSplat.attributes;
var hmAsset = pc.app.assets._assets.find(function(asset) {
return asset._id == opts.heightMap;
});
var groundMat = pc.app.assets._assets.find(function(asset) {
return asset._id == opts.material;
});
var splatShader = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.splatShader;
});
var splatMap = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.splatTex;
});
var redTex = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.redTex;
});
var greenTex = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.greenTex;
});
var blueTex = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.blueTex;
});
var alphaTex = pc.app.assets._assets.find(function(asset) {
return asset._id == shaderOpts.alphaTex;
});
var assetsToLoad = [hmAsset, groundMat, splatShader, splatMap, redTex, greenTex, blueTex, alphaTex];
var count = 0;
assetsToLoad.forEach(
function(assetToLoad) {
assetToLoad.ready(function(asset) {
count++;
if (count === assetsToLoad.length) {
var img = hmAsset.resource.getSource();
opts.material = groundMat.resource;
opts.splatShader = splatShader.resource;
opts.splatMap = splatMap.resource;
opts.redTex = redTex.resource;
opts.greenTex = greenTex.resource;
opts.blueTex = blueTex.resource;
opts.alphaTex = alphaTex.resource;
var meshInstances = createTerrainFromHeightMap(img, opts);
var entity = new pc.Entity("heightmap");
pc.app.root.addChild(entity);
entity.addComponent('render', {
type: 'asset',
meshInstances: meshInstances,
layers: [pc.app.scene.layers.getLayerByName("World").id]
});
var c_mat = entity.render.meshInstances[0].material.clone();
c_mat.chunks.diffusePS = opts.splatShader;
c_mat.update();
c_mat.setParameter('splatMap', opts.splatMap);
c_mat.setParameter('texture_red', opts.redTex);
c_mat.setParameter('texture_green', opts.greenTex);
c_mat.setParameter('texture_blue', opts.blueTex);
c_mat.setParameter('texture_alpha', opts.alphaTex);
c_mat.setParameter('scale_factor', opts.height);
entity.render.meshInstances[0].material = c_mat;
}
});
pc.app.assets.load(assetToLoad);
}
);
};
function createTerrainVertexData(options) {
var positions = [];
var uvs = [];
var indices = [];
var row, col;
for (row = 0; row <= options.subdivisions; row++) {
for (col = 0; col <= options.subdivisions; col++) {
var position = new pc.Vec3((col * options.width) / options.subdivisions - (options.width / 2.0), 0, ((options.subdivisions - row) * options.height) / options.subdivisions - (options.height / 2.0));
var heightMapX = (((position.x + options.width / 2) / options.width) * (options.bufferWidth - 1)) | 0;
var heightMapY = ((1.0 - (position.z + options.height / 2) / options.height) * (options.bufferHeight - 1)) | 0;
var pos = (heightMapX + heightMapY * options.bufferWidth) * 4;
var r = options.buffer[pos] / 255.0;
var g = options.buffer[pos + 1] / 255.0;
var b = options.buffer[pos + 2] / 255.0;
var gradient = r * 0.3 + g * 0.59 + b * 0.11;
position.y = options.minHeight + (options.maxHeight - options.minHeight) * gradient;
positions.push(position.x, position.y, position.z);
uvs.push(col / options.subdivisions, 1.0 - row / options.subdivisions);
}
}
for (row = 0; row < options.subdivisions; row++) {
for (col = 0; col < options.subdivisions; col++) {
indices.push(col + row * (options.subdivisions + 1));
indices.push(col + 1 + row * (options.subdivisions + 1));
indices.push(col + 1 + (row + 1) * (options.subdivisions + 1));
indices.push(col + row * (options.subdivisions + 1));
indices.push(col + 1 + (row + 1) * (options.subdivisions + 1));
indices.push(col + (row + 1) * (options.subdivisions + 1));
}
}
var normals = pc.calculateNormals(positions, indices);
return {
indices: indices,
positions: positions,
normals: normals,
uvs: uvs
};
};
function createTerrainFromHeightMap(img, opts) {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
var bufferWidth = img.width;
var bufferHeight = img.height;
canvas.width = bufferWidth;
canvas.height = bufferHeight;
context.drawImage(img, 0, 0);
var buffer = context.getImageData(0, 0, bufferWidth, bufferHeight).data;
var vertexData = createTerrainVertexData({
width: opts.width,
height: opts.height,
subdivisions: opts.subdivisions,
minHeight: opts.minHeight,
maxHeight: opts.maxHeight,
buffer: buffer,
bufferWidth: bufferWidth,
bufferHeight: bufferHeight
});
var mesh = pc.createMesh(pc.app.graphicsDevice, vertexData.positions, {
normals: vertexData.normals,
uvs: vertexData.uvs,
indices: vertexData.indices
});
var meshInstance = new pc.MeshInstance(mesh, opts.material);
return [meshInstance];
};
var cc = document.getElementsByClassName("top-controls");
if (cc) {
var ter = document.createElement("div");
ter.className = "ui-button noHeader camera";
ter.textContent = "Render Terrain";
cc[0].children[0].appendChild(ter);
ter.addEventListener('click', function(event) {
run();
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment