Autodesk Forge is now Autodesk Platform Services

15 Aug 2016

Ace Editor for Three.js ShaderMaterials in the Forge Viewer

Default blog image

Thumb

 

By Michael Ge (@hahakumquat)

Reloading the viewer every time you make a change can be a hassle. Fortunately,  we can skip the refresh time by updating the scene's code directly within the viewer.

Here is an extension that implements a dynamically sizing Ace Editor within the viewer. In this case, I used the editor to write WebGL shader code that would update a selected fragment's material.

Creating the Editor Panel

Philippe's post does a good job explaining how to make a panel from a DockingPanel. From there, it's just a matter of injecting the editor. Simply calling ace.edit("containerId") will create an editor (wrapped in a div) within the containerId div.

The Ace Editor has a .resize() function that resizes an editor to its parent element's size. We can thus use this to our advantage by calling .resize() every time containerId is resized. Here is a good jsfiddle that shows the correct DOM hierarchy. In order to check for a resize event, I used the following script which resizes whenever the mouse is down and moving:

$("#" + baseId)
.mousedown(function() {
isDragging = false;
})
.mousemove(function() {
var wasDragging = isDragging;
isDragging = true;
if (wasDragging)
editor.resize();
})
.mouseup(function() {
var wasDragging = isDragging;
isDragging = false;
if (!wasDragging) {
editor.resize();
}
});
view raw drag.js hosted with ❤ by GitHub

Validating the Shaders

Since the ace editor doesn't come with built-in GLSL error checking, I found a workaround by bkCore's shader editor example that compiles a test WebGL shader and prints out the errors. Some issues I ran into were errors that Three.js handles when creating a ShaderMaterial, so I did some error checking to parse out unnecessary errors.

// Adapted from Shdr Validator class https://github.com/BKcore/Shdr
// Creates and validates a shader from a text source based on type
// (src, type) -> false || [ok, line, error]
// src: glsl text to be validated
// type: 0 for vertex shader, 1 for fragment shader, else return false
// ok: boolean for whether the shader is ok or not
// line: which line number throws error (only if ok is false)
// error: description of error (only if ok is false and line != null)
function validate(src, type) {
// uniforms don't get validated by glsl
if (type !== 0 && type !== 1) {
return false;
}
if (!src) {
return [false, 0, "Shader cannot be empty"];
}
if (!context) {
console.warn("No WebGL context.");
}
var details, error, i, line, lines, log, message, shader, status, _i, _len;
try {
var shaderType = type === 0 ? context.VERTEX_SHADER : context.FRAGMENT_SHADER;
shader = context.createShader(shaderType);
context.shaderSource(shader, src);
context.compileShader(shader);
status = context.getShaderParameter(shader, context.COMPILE_STATUS);
}
catch(e) {
return [false, 0, e.getMessage];
}
if (status === true) {
return [true, null, null];
}
else {
// filters out THREE.js handled errors in the raw log
log = context.getShaderInfoLog(shader);
var rawLog = log;
lines = rawLog.split('\n');
for (_i = 0, _len = lines.length; _i < _len; _i++) {
i = lines[_i];
if (i.substr(0, 5) === 'ERROR') {
if (i.indexOf("undeclared identifier") > -1) {
if (i.indexOf("projectionMatrix") > -1 ||
i.indexOf("modelMatrix") > -1 ||
i.indexOf("modelViewMatrix") > -1 ||
i.indexOf("viewMatrix") > -1 ||
i.indexOf("cameraPosition") > -1 ||
i.indexOf("normal") > -1 ||
i.indexOf("uv") > -1 ||
i.indexOf("uv2") > -1 ||
i.indexOf("position") > -1) {
lines.splice(_i, 1);
_i--;
_len--;
}
}
else if (i.indexOf("No precision specified for (float)") > -1) {
lines.splice(_i, 1);
_i--;
_len--;
}
else if (i.indexOf("'constructor' : not enough data provided for construction") > -1) {
lines.splice(_i, 1);
_i--;
_len--;
}
}
}
for (_i = 0, _len = lines.length; _i < _len; _i++) {
i = lines[_i];
if (i.substr(0, 5) === 'ERROR') {
error = i;
}
}
if (!error || error[0] === "") {
return [true, null, null];
// return [false, 0, 'Unable to parse error.'];
}
details = error.split(':');
if (details.length < 4) {
return [false, 0, error];
}
line = details[2];
message = details.splice(3).join(':');
return [false, parseInt(line), message];
}
}
view raw validator.js hosted with ❤ by GitHub

Displaying the Shaders

Displaying the shaders uses a similar function I made for a heatmap demo I made a while back:

function setMaterial(fragId) {
var material = new THREE.ShaderMaterial({
uniforms: eval('('+uniformDocument.getValue()+')'),
vertexShader: vertexDocument.getValue(),
fragmentShader: fragmentDocument.getValue(),
side: THREE.DoubleSide
});
_viewer.impl.matman().removeMaterial("shaderMaterial");
_viewer.impl.matman().addMaterial("shaderMaterial", material, true);
_viewer.model.getFragmentList().setMaterial(fragId, material);
_viewer.impl.invalidate(true);
}
view raw setMaterial.js hosted with ❤ by GitHub

An interesting thing to note is that, in my demo, I allowed the user to input uniforms via a javascript JSON object and running the string with an eval call. I'd typically suggest against doing that, as such a feature might leave your website vulnerable to malicious attacks.

That said, the demo poses some interesting applications. I'll be looking into writing entire extensions within viewer panels like these as well that affect not only the textures, but also the geometry and other scene states as well.

View the complete sample here.

Related Article