elation-engine
Version:
WebGL/WebVR engine written in Javascript
482 lines (456 loc) • 22 kB
JavaScript
elation.require(['utils.template', 'engine.external.three.three'], function() {
elation.requireCSS('engine.materials');
// FIXME - these templates are needed by the editor, but there's no reason to initialize them synchronously for every engine load
// Material editor isn't currently exposed anyway - we should split this out to be part of the admin system
/*
if (elation.env.isBrowser) {
elation.template.add('engine.materials.chunk', '<div class="engine_materials_chunk style_box"><h1>{chunkname}</h1><div class="engine_materials_chunk_uniforms style_box"><h2>uniforms</h2> <ul> {#uniforms} <li><h4>{name}</h4> ({type})</li> {/uniforms} </ul></div> <div class="engine_materials_chunk_vertex style_box"><h2>vertex</h2><p>{chunk.vertex_pars}</p><p>{chunk.vertex}</p></div> <div class="engine_materials_chunk_fragment style_box"><h2>fragment</h2><p>{chunk.fragment_pars}</p><p>{chunk.fragment}</p></div></div>');
elation.template.add('engine.materials.chunk.uniforms', '<h3>{chunkname}</h3><ul class="engine_materials_chunk_uniform"> {#uniforms} <li><h4>{name}</h4> <input value="{value}"> ({type})</li> {/uniforms} </ul>');
elation.template.add('engine.materials.chunk.vertex', '<h3>{chunkname}</h3><p elation:component="engine.materials.editor" elation:args.chunkname="{chunkname}" elation:args.chunktype="vertex" {?params}elation:args.params=1{/params} class="engine_materials_chunk_vertex">{content}</p>');
elation.template.add('engine.materials.chunk.fragment', '<h3>{chunkname}</h3><p elation:component="engine.materials.editor" elation:args.chunkname="{chunkname}" elation:args.chunktype="fragment" {?params}elation:args.params=1{/params} class="engine_materials_chunk_fragment">{content}</p>');
}
*/
elation.extend("engine.materials", new function() {
this.shaders = {};
this.shaderdefs = {};
this.shaderchunks = {};
this.texturecache = {};
this.materialinstances = {};
this.materiallibrary = {};
this.get = function(args) {
if (elation.utils.isString(args) && this.materiallibrary[args]) {
return this.materiallibrary[args].clone();
} else {
var materialtype = THREE.MeshPhongMaterial; //(Detector.webgl ? THREE.MeshPhongMaterial : THREE.MeshBasicMaterial);
return new materialtype(args);
}
}
this.add = function(materialname, materialargs) {
if (materialargs instanceof THREE.Material) {
this.materiallibrary[materialname] = materialargs;
} else {
this.materiallibrary[materialname] = this.get(materialargs);
}
elation.events.fire({element: this, type: 'engine_material_add', data: { name: materialname, material: this.materiallibrary[materialname] } });
}
this.getTexture = function(url, repeat, mirrored) {
if (elation.env.isNode) return;
if (!this.texturecache[url]) {
if (url.match(/^data:/)) {
var img = document.createElement('IMG');
img.src = url;
this.texturecache[url] = new THREE.Texture(img);
//this.texturecache[url].needsUpdate = true;
this.texturecache[url].sourceFile = url;
} else if (url.match(/\.dds$/)) {
/*
var ddsloader = new THREE.DDSLoader();
this.texturecache[url] = ddsloader.load(url);
//this.texturecache[url].flipY = false;
*/
} else {
THREE.ImageUtils.crossOrigin = '';
var texture = this.texturecache[url] = THREE.ImageUtils.loadTexture(url, undefined, elation.bind(this, function() {
var image = texture.image;
image.crossOrigin = '';
if (!this.isPowerOfTwo(image.width) || !this.isPowerOfTwo(image.height)) {
// Scale up the texture to the next highest power of two dimensions.
var canvas = document.createElement("canvas");
canvas.width = this.nextHighestPowerOfTwo(image.width);
canvas.height = this.nextHighestPowerOfTwo(image.height);
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
texture.image = canvas;
//texture.needsUpdate = true;
}
elation.events.fire({element: texture, type: 'engine_texture_load'});
}));
//elation.events.fire({ type: 'resource_load_start', data: { type: 'image', image: this.texturecache[url].image } });
}
if (!this.texturecache[url]) {
this.texturecache[url] = elation.engine.materials.getTexture('/media/space/textures/uvtest.png', [1, 1]);
}
this.texturecache[url].anisotropy = 16;
elation.events.add(this.texturecache[url], 'update', function(ev) {
elation.events.fire({ type: 'engine_texture_load', element: ev.target });
//elation.events.fire({ type: 'resource_load_finish', data: { type: 'image', image: ev.target.image } });
});
}
if (repeat) {
this.setTextureRepeat(this.texturecache[url], repeat, mirrored);
}
return this.texturecache[url];
}
this.setTextureRepeat = function(texture, repeat, mirrored) {
texture.wrapS = (mirrored ? THREE.MirroredRepeatWrapping : THREE.RepeatWrapping);
texture.wrapT = (mirrored ? THREE.MirroredRepeatWrapping : THREE.RepeatWrapping);
if (repeat instanceof THREE.Vector2) {
texture.repeat.copy(repeat);
} else if (elation.utils.isArray(repeat)) {
texture.repeat.set(repeat[0], repeat[1]);
}
}
this.isPowerOfTwo = function(num) {
return (num & (num - 1)) == 0;
}
this.nextHighestPowerOfTwo = function(num) {
num--;
for (var i = 1; i < 32; i <<= 1) {
num = num | num >> i;
}
return num + 1;
}
this.addShader = function(shadername, shader) {
this.shaders[shadername] = shader;
}
this.getShaderMaterial = function(shadername, uniforms, defines, lights) {
if (this.shaders[shadername]) {
var prefix = '';
var shaderargs = {
vertexShader: this.shaders[shadername].vertexShader,
fragmentShader: this.shaders[shadername].fragmentShader,
//lights: (typeof lights != 'undefined' ? lights : false),
//perPixel: true, fog: true, map: true
};
if (defines) {
shaderargs.defines = defines;
}
shaderargs.uniforms = THREE.UniformsUtils.clone( this.shaders[shadername].uniforms );
if (this.shaders[shadername].attributes) {
shaderargs.attributes = this.shaders[shadername].attributes;
}
for (var k in uniforms) {
if (shaderargs.uniforms[k]) {
shaderargs.uniforms[k].value = uniforms[k];
}
}
var shader = new THREE.ShaderMaterial(shaderargs);
// Store reference to material instance so we can refresh it if needed later
if (!this.materialinstances[shadername]) this.materialinstances[shadername] = [];
this.materialinstances[shadername].push(shader);
return shader;
}
return new THREE.MeshBasicMaterial({color: 0xcc0000});
}
this.buildShader = function(shadername, chunkargs) {
this.shaderdefs[shadername] = chunkargs;
var vertex_parms = vertex_shader = fragment_parms = fragment_shader = "";
var shaderargs = {
vertexShader: THREE.ShaderLib['lambert'].vertexShader,
fragmentShader: THREE.ShaderLib['lambert'].fragmentShader,
lights: true,
perPixel: true
};
if (chunkargs.chunks_vertex) {
shaderargs.vertexShader = this.assembleChunks('vertex', chunkargs.chunks_vertex);
}
if (chunkargs.chunks_fragment) {
shaderargs.fragmentShader = this.assembleChunks('fragment', chunkargs.chunks_fragment);
}
if (chunkargs.uniforms) {
var uniforms = [];
for (var i = 0; i < chunkargs.uniforms.length; i++) {
var chunkname = chunkargs.uniforms[i];
if (this.shaderchunks[chunkname] && this.shaderchunks[chunkname].uniforms) {
uniforms.push(this.shaderchunks[chunkname].uniforms);
} else if (THREE.UniformsLib[chunkname]) {
uniforms.push(THREE.UniformsLib[chunkname]);
}
}
shaderargs.uniforms = THREE.UniformsUtils.merge(uniforms);
}
if (chunkargs.attributes) {
shaderargs.attributes = attributes;
var attributes = [];
for (var i = 0; i < chunkargs.attributes.length; i++) {
var chunkname = chunkargs.attributes[i];
if (this.shaderchunks[chunkname] && this.shaderchunks[chunkname].attributes) {
attributes.push(this.shaderchunks[chunkname].attributes);
}
}
shaderargs.attributes = THREE.UniformsUtils.merge(attributes);
}
this.shaders[shadername] = shaderargs;
}
this.addChunk = function(chunkname, chunkargs) {
this.shaderchunks[chunkname] = chunkargs;
}
this.setChunk = function(chunkname, chunktype, value) {
if (!this.shaderchunks[chunkname]) {
var chunkargs = {};
chunkargs[chunktype] = value;
this.addChunk(chunkname, chunkargs);
} else {
this.shaderchunks[chunkname][chunktype] = value;
}
}
this.assembleChunks = function(type, chunks) {
var chunk = ["", ""];
chunk[0] = THREE.ShaderChunk['common'];
for (var i = 0; i < chunks.length; i++) {
var chunkname = chunks[i];
var chunkpart = ["", ""];
if (this.shaderchunks[chunkname]) {
if (this.shaderchunks[chunkname]['common_pars']) {
chunkpart[0] += this.shaderchunks[chunkname]['common_pars'] + "\n";
}
if (this.shaderchunks[chunkname][type + '_pars']) {
chunkpart[0] += this.shaderchunks[chunkname][type + '_pars'] + "\n";
}
if (this.shaderchunks[chunkname][type]) {
chunkpart[1] += this.shaderchunks[chunkname][type] + "\n";
}
}
var idxpars = chunkname + '_pars_' + type,
idxshader = chunkname + '_' + type;
//console.log(idxpars, THREE.ShaderChunk[idxpars]);
//console.log(idxshader, THREE.ShaderChunk[idxshader]);
if (chunkpart[0] == "" && THREE.ShaderChunk[idxpars]) {
chunkpart[0] += THREE.ShaderChunk[idxpars] + "\n";
}
if (chunkpart[1] == "" && THREE.ShaderChunk[idxshader]) {
chunkpart[1] += THREE.ShaderChunk[idxshader] + "\n";
}
chunk[0] += chunkpart[0];
chunk[1] += chunkpart[1];
}
return chunk[0] + "\nvoid main() {\n" + chunk[1] + "\n}\n";
}
this.displaychunks = function(shaders) {
/*
if (!chunks) chunks = this.shaderchunks;
for (var k in chunks) {
var chunkwrapper = elation.html.create({tag: 'div', className: 'engine_materials_chunk', append: root});
var tplargs = {
chunkname: k,
chunk: chunks[k],
uniforms: []
};
for (var j in chunks[k].uniforms) {
chunks[k].uniforms[j].name = j;
tplargs.uniforms.push(chunks[k].uniforms[j]);
}
chunkwrapper.innerHTML = elation.template.get("engine.materials.chunk", tplargs);
console.log(chunks[k], chunks[k].uniforms);
}
*/
}
this.displayall = function(shaders, material) {
if (!shaders) shaders = this.shaderdefs;
for (var k in shaders) {
this.display(k, material);
}
}
this.display = function(shadername, material) {
var shaderdef = this.shaderdefs[shadername];
//console.log(shadername, shaderdef, material);
var root = elation.html.create({type: 'div', classname: 'engine_material style_box'});
elation.ui.window({title: shadername, append: document.body, content: root});
var uniformcontainer = elation.html.create({classname: 'engine_materials_uniforms style_box', content: '<h2>Uniforms</h2>', append: root});
var vertexcontainer = elation.html.create({classname: 'engine_materials_vertex style_box', content: '<h2>Vertex Shader</h2>', append: root});
var fragmentcontainer = elation.html.create({classname: 'engine_materials_fragment style_box', content: '<h2>Fragment Shader</h2>', append: root});
var vertexpars = elation.html.create({classname: 'engine_materials_shader_params', append: vertexcontainer});
var vertexshader = elation.html.create({classname: 'engine_materials_shader_main', append: vertexcontainer});
var fragmentpars = elation.html.create({classname: 'engine_materials_shader_params', append: fragmentcontainer});
var fragmentshader = elation.html.create({classname: 'engine_materials_shader_main', append: fragmentcontainer});
/* uniforms */
for (var j in shaderdef.uniforms) {
var chunkname = shaderdef.uniforms[j];
var tplargs = {
shadername: shadername,
chunkname: chunkname,
uniforms: []
};
if (this.shaderchunks[chunkname]) {
for (var l in this.shaderchunks[chunkname].uniforms) {
var uniform = this.shaderchunks[chunkname].uniforms[l];
uniform.name = l;
if (material && material.uniforms[l]) {
var uval = material.uniforms[l].value;
if (uniform.type == 'c') uniform.value = '#' + uval.getHexString();
else if (uniform.type == 'v2') uniform.value = uval.x + ',' + uval.y;
else if (uniform.type == 'v3') uniform.value = uval.x + ',' + uval.y + ',' + uval.z;
else if (uniform.type == 'v4') uniform.value = uval.x + ',' + uval.y + ',' + uval.z + ',' + uval.w;
else uniform.value = material.uniforms[l].value;
}
tplargs.uniforms.push(uniform);
}
} else {
if (THREE.UniformsLib[chunkname]) {
for (var l in THREE.UniformsLib[chunkname]) {
var uniform = THREE.UniformsLib[chunkname][l];
uniform.name = l;
if (material && material.uniforms[l]) {
var uval = material.uniforms[l].value;
if (uniform.type == 'c') uniform.value = '#' + uval.getHexString();
else if (uniform.type == 'v2') uniform.value = uval.x + ',' + uval.y;
else if (uniform.type == 'v3') uniform.value = uval.x + ',' + uval.y + ',' + uval.z;
else if (uniform.type == 'v4') uniform.value = uval.x + ',' + uval.y + ',' + uval.z + ',' + uval.w;
else uniform.value = material.uniforms[l].value;
}
tplargs.uniforms.push(uniform);
}
}
}
elation.html.create({tag: 'div', classname: 'engine_materials_chunk', append: uniformcontainer, content: elation.template.get("engine.materials.chunk.uniforms", tplargs)});
}
/* vertex */
for (var j in shaderdef.chunks_vertex) {
var chunkname = shaderdef.chunks_vertex[j];
var tplargs = {
shadername: shadername,
chunkname: chunkname
};
if (this.shaderchunks[chunkname]) {
tplargs.vertex_pars = this.shaderchunks[chunkname].vertex_pars;
tplargs.vertex = this.shaderchunks[chunkname].vertex;
}
if (!tplargs.vertex_pars && THREE.ShaderChunk[chunkname + '_pars_vertex']) {
tplargs.vertex_pars = THREE.ShaderChunk[chunkname + '_pars_vertex'];
}
if (!tplargs.vertex && THREE.ShaderChunk[chunkname + '_vertex']) {
tplargs.vertex = THREE.ShaderChunk[chunkname + '_vertex'];
}
if (tplargs.vertex_pars) {
tplargs.content = tplargs.vertex_pars;
tplargs.params = true;
elation.html.create({tag: 'div', classname: 'engine_materials_chunk', append: vertexpars, content: elation.template.get("engine.materials.chunk.vertex", tplargs)});
}
if (tplargs.vertex) {
tplargs.content = tplargs.vertex;
tplargs.params = false;
elation.html.create({tag: 'div', classname: 'engine_materials_chunk', append: vertexshader, content: elation.template.get("engine.materials.chunk.vertex", tplargs)});
}
}
/* fragment */
for (var j in shaderdef.chunks_fragment) {
var chunkname = shaderdef.chunks_fragment[j];
var tplargs = {
shadername: shadername,
chunkname: chunkname
};
if (this.shaderchunks[chunkname]) {
tplargs.fragment_pars = this.shaderchunks[chunkname].fragment_pars;
tplargs.fragment = this.shaderchunks[chunkname].fragment;
}
if (!tplargs.fragment_pars && THREE.ShaderChunk[chunkname + '_pars_fragment']) {
tplargs.fragment_pars = THREE.ShaderChunk[chunkname + '_pars_fragment'];
}
if (!tplargs.fragment && THREE.ShaderChunk[chunkname + '_fragment']) {
tplargs.fragment = THREE.ShaderChunk[chunkname + '_fragment'];
}
if (tplargs.fragment_pars) {
tplargs.content = tplargs.fragment_pars;
tplargs.params = true;
elation.html.create({tag: 'div', classname: 'engine_materials_chunk', append: fragmentpars, content: elation.template.get("engine.materials.chunk.fragment", tplargs)});
}
if (tplargs.fragment) {
tplargs.content = tplargs.fragment;
tplargs.params = false;
elation.html.create({tag: 'div', classname: 'engine_materials_chunk', append: fragmentshader, content: elation.template.get("engine.materials.chunk.fragment", tplargs)});
}
}
elation.component.init();
var editors = elation.find('.engine_materials_editor', this.container);
for (var i = 0; i < editors.length; i++) {
var component = elation.component.fetch(editors[i]);
elation.events.add(component, 'engine_material_change', this);
}
}
this.handleEvent = elation.events.handleEvent;
this.engine_material_change = function(ev) {
var editor = ev.target;
var chunktype = editor.chunktype + (editor.params ? '_pars' : '');
this.setChunk(editor.chunkname, chunktype, editor.content);
// FIXME - need some sort of shader pre-compile check to make sure it's valid before we start using it in the scene
for (var k in this.shaderdefs) {
var deftype = 'chunks_' + editor.chunktype;
if (this.shaderdefs[k][deftype].indexOf(editor.chunkname) != -1) {
this.buildShader(k, this.shaderdefs[k]);
if (this.materialinstances[k]) {
for (var j = 0; j < this.materialinstances[k].length; j++) {
this.materialinstances[k][j].fragmentShader = this.shaderdef.fragmentShader;
this.materialinstances[k][j].vertexShader = this.shaderdef.vertexShader;
this.materialinstances[k][j].needsUpdate = true;
}
}
}
}
}
this.getTextureLabel = function(text, fontsize, color, font, background) {
var c = elation.html.create('canvas');
var ctx = c.getContext('2d');
if (fontsize === undefined) fontsize = 32;
if (color === undefined) color = '#fff';
if (font === undefined) font = 'serif';
if (background === undefined) background = 'rgba(0,0,0,0)';
if (color instanceof THREE.Color) {
color = 'rgb(' + color.toArray().map(function(x) { return x * 255; }).join(', ') + ')';
}
ctx.font = fontsize + 'px ' + font;
var size = ctx.measureText(text);
c.width = size.width;
c.height = fontsize;
// changing width resets context, so reset size
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
ctx.font = fontsize + 'px ' + font;
ctx.fillStyle = color;
ctx.fillText(text, 0, fontsize - fontsize/6);
ctx.strokeText(text, 0, fontsize - fontsize/6);
/*
document.body.appendChild(c);
c.style.position = 'fixed';
c.style.top = c.style.left = '50%';
c.style.border = '2px solid red';
c.style.zIndex = 4500;
*/
var tex = new THREE.Texture(c);
tex.needsUpdate = true;
return tex;
}
});
elation.component.add("engine.materials.editor", function() {
this.changetimeout = 500;
this.init = function() {
this.chunkname = this.args.chunkname;
this.chunktype = this.args.chunktype;
this.params = this.args.params || false;
elation.html.addclass(this.container, "engine_materials_editor");
//this.container.contentEditable = true;
this.container.spellcheck = false;
this.content = this.container.innerText;
elation.events.add(this.container, "click,focus,blur,keyup", this);
}
this.change = function() {
var newtext = this.container.innerText;
if (newtext != this.content) {
console.log('cool', newtext);
this.content = newtext;
elation.events.fire({element: this, type: 'engine_material_change', data: newtext});
}
}
this.click = function(ev) {
this.container.style.width = this.container.scrollWidth + 'px';
this.container.contentEditable = true;
this.container.focus();
}
this.focus = function(ev) {
//document.designMode = 'on';
}
this.blur = function(ev) {
//document.designMode = 'off';
this.change();
this.container.style.width = 'auto';
this.container.contentEditable = false;
}
this.keyup = function(ev) {
if (this.changetimer) clearTimeout(this.changetimer);
this.changetimer = setTimeout(elation.bind(this, this.change), this.changetimeout);
}
});
if (elation.env.isBrowser) {
//elation.engine.materials.add('uvtest', new THREE.MeshPhongMaterial({map: elation.engine.materials.getTexture('/media/space/textures/uvtest.png', [1, 1])}));
}
elation.engine.materials.add('normal', new THREE.MeshNormalMaterial());
});