/*
* Copyright 2021 Markus Heimerl, OTH Regensburg
* Licensed under CC BY-NC 4.0
*
* ANY USE OF THIS SOFTWARE MUST COMPLY WITH THE
* CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL 4.0 INTERNATIONAL LICENSE
* AGREEMENTS
*/
/*
optimization:
- use gl.drawElements instead of gl.drawArrays and draw each cube using 8 instead of 36 verticies (4.5 times less!) - tried it. Its not faster at all and I dont know why. Can be found under /cellularautomata/cellauto3ddrawElements.js
*/
cellularautomata3d();
function cellularautomata3d(){
function parseRulestring3D(rulestring){
var res = rulestring.split("/");
res[0] = res[0].substring(1);
res[1] = res[1].substring(1);
var bornValues = [];
if(res[0] == ""){
bornValues[0] = -9999;
}else{
var commasplit = res[0].split(",");
for(var i = 0; i < commasplit.length; i++){
bornValues[i] = parseInt(commasplit[i]);
}
}
var surviveValues = [];
if(res[1] == ""){
surviveValues[1] = -9999;
}else{
var commasplit = res[1].split(",");
for(var i = 0; i < commasplit.length; i++){
surviveValues[i] = parseInt(commasplit[i]);
}
}
return [bornValues, surviveValues];
}
function checkRuleConditions(neighborcount, values){
for(var i = 0; i < values.length; i++){
if(neighborcount == values[i]) return true;
}
return false;
}
function createCellGridWireframe(cellularworldsize, randomize){
var cellgrid = [];
for(var x = 0; x < cellularworldsize; x++){
cellgrid[x] = [];
for(var y = 0; y < cellularworldsize; y++){
cellgrid[x][y] = [];
for(var z = 0; z < cellularworldsize; z++){
if(randomize)cellgrid[x][y][z] = Math.random() >= 0.5 ? 1 : 0;
else cellgrid[x][y][z] = 0;
// mark world edges
if(y == 0 && z == 0) cellgrid[x][y][z] = 1;
if(x == 0 && z == 0) cellgrid[x][y][z] = 1;
if(x == 0 && y == 0) cellgrid[x][y][z] = 1;
if(y == cellularworldsize-1 && z == cellularworldsize-1) cellgrid[x][y][z] = 1;
if(x == cellularworldsize-1 && z == cellularworldsize-1) cellgrid[x][y][z] = 1;
if(x == cellularworldsize-1 && y == cellularworldsize-1) cellgrid[x][y][z] = 1;
if(y == cellularworldsize-1 && z == 0) cellgrid[x][y][z] = 1;
if(x == cellularworldsize-1 && z == 0) cellgrid[x][y][z] = 1;
if(x == cellularworldsize-1 && y == 0) cellgrid[x][y][z] = 1;
if(y == 0 && z == cellularworldsize-1) cellgrid[x][y][z] = 1;
if(x == 0 && z == cellularworldsize-1) cellgrid[x][y][z] = 1;
if(x == 0 && y == cellularworldsize-1) cellgrid[x][y][z] = 1;
}
}
}
return cellgrid;
}
var cellularworldsize = 60;
var cellgrid = createCellGridWireframe(cellularworldsize, true);
var survivevalues = parseRulestring3D($("#cellRuleInput3D").val())[1];
var bornvalues = parseRulestring3D($("#cellRuleInput3D").val())[0];
cellularAutomataLogic();
function cellularAutomataLogic(){
if(running){
var cellgridnextframe = createCellGridWireframe(cellularworldsize, false);
for(var x = 2; x < cellularworldsize-2; x++){
for(var y = 2; y < cellularworldsize-2; y++){
for(var z = 2; z < cellularworldsize-2; z++){
// get cell status
var status = cellgrid[x][y][z];
// get neighbor count
var zm1 = z-1 < 2 ? cellularworldsize-3 : z-1;
var zp1 = z+1 == cellularworldsize-2 ? 2 : z+1;
var ym1 = y-1 < 2 ? cellularworldsize-3 : y-1;
var yp1 = y+1 == cellularworldsize-2 ? 2 : y+1;
var xm1 = x-1 < 2 ? cellularworldsize-3 : x-1;
var xp1 = x+1 == cellularworldsize-2 ? 2 : x+1;
// below
var neighborcount = cellgrid[x][y][zm1] + cellgrid[x][yp1][zm1] + cellgrid[x][ym1][zm1] + cellgrid[xp1][y][zm1]
+ cellgrid[xm1][y][zm1] + cellgrid[xp1][yp1][zm1] + cellgrid[xp1][ym1][zm1] + cellgrid[xm1][ym1][zm1] + cellgrid[xm1][yp1][zm1];
// level
neighborcount += cellgrid[x][yp1][z] + cellgrid[x][ym1][z] + cellgrid[xp1][y][z]
+ cellgrid[xm1][y][z] + cellgrid[xp1][yp1][z] + cellgrid[xp1][ym1][z] + cellgrid[xm1][ym1][z] + cellgrid[xm1][yp1][z];
// above
neighborcount += cellgrid[x][y][zp1] + cellgrid[x][yp1][zp1] + cellgrid[x][ym1][zp1] + cellgrid[xp1][y][zp1]
+ cellgrid[xm1][y][zp1] + cellgrid[xp1][yp1][zp1] + cellgrid[xp1][ym1][zp1] + cellgrid[xm1][ym1][zp1] + cellgrid[xm1][yp1][zp1];
// if cell alive and neighborcount 2,3 or 8, it stays alive
if(status == 1 && checkRuleConditions(neighborcount, survivevalues)){
cellgridnextframe[x][y][z] = 1;
}
// if cell is dead and neighborcount is 3, it is born
else if(status == 0 && checkRuleConditions(neighborcount, bornvalues)){
cellgridnextframe[x][y][z] = 1;
}
// in all other cases the cell remains dead or dies (already initialized as 0)
}
}
}
cellgrid = cellgridnextframe;
}
setTimeout(cellularAutomataLogic, 100);
}
// --- DRAWING CODE ---
function parseOBJ(data){
var allVerticies = [];
var allTexCoords = [];
var allNormals = [];
var objects = {};
var objname = "";
var individualLines = data.split('\n');
for(var i = 0; i < individualLines.length; i++){
var lineElements = individualLines[i].split(' ');
var type = lineElements[0];
switch(type){
case "#":
break;
case " ":
break;
case "o":
objname = lineElements[1];
objects[objname] = {};
objects[objname].positions = [];
objects[objname].texcoords = [];
objects[objname].normals = [];
break;
case "v":
allVerticies.push([lineElements[1], lineElements[2], lineElements[3]].map(parseFloat));
break;
case "vt":
allTexCoords.push([lineElements[1], lineElements[2]].map(parseFloat));
break;
case "vn":
allNormals.push([lineElements[1], lineElements[2], lineElements[3]].map(parseFloat));
break;
case "f":
var faceParts = [];
for(var j = 0; j < lineElements.length - 1; j++){
faceParts[j] = lineElements[j+1].split('/').map(str => parseInt(str)-1);
}
objects[objname].positions.push(allVerticies[faceParts[0][0]], allVerticies[faceParts[1][0]], allVerticies[faceParts[2][0]]);
objects[objname].texcoords.push(allTexCoords[faceParts[0][1]], allTexCoords[faceParts[1][1]], allTexCoords[faceParts[2][1]]);
objects[objname].normals.push(allNormals[faceParts[0][2]], allNormals[faceParts[1][2]], allNormals[faceParts[2][2]]);
break;
default:
//console.warn("objloader: unhandled keyword: ", type);
}
}
for(var obj in objects){
objects[obj].positions = objects[obj].positions.flat();
objects[obj].texcoords = objects[obj].texcoords.flat();
objects[obj].normals = objects[obj].normals.flat();
}
return objects;
}
const vertexshadersource = `
precision highp float;
attribute vec4 vertexposition;
attribute vec2 texturecoordinate;
attribute vec3 normal;
uniform mat4 modelmatrix;
uniform mat4 projectionmatrix;
uniform mat4 viewmatrix;
varying vec2 o_texturecoordinate;
varying vec3 o_normal;
void main(){
o_texturecoordinate = texturecoordinate;
o_normal = mat3(modelmatrix) * normal;
gl_Position = projectionmatrix * viewmatrix * modelmatrix * vertexposition;
}
`;
const fragmentshadersource = `
precision highp float;
varying vec2 o_texturecoordinate;
varying vec3 o_normal;
uniform sampler2D texture;
uniform vec3 reverseLightDirection;
void main(){
vec3 normal = normalize(o_normal);
float light = dot(normal, reverseLightDirection);
gl_FragColor = texture2D(texture, o_texturecoordinate);
//gl_FragColor.rgb *= light;
}
`;
// --- GET CANVAS CONTEXT AND SETUP KEY LISTENERS ---
var running = true;
var fscanvas = $("#cellauto3dcanvas")[0];
fscanvas.width = Math.pow(2, Math.floor(getBaseLog(2, $(window).width() * 0.88)));
window.addEventListener("orientationchange", function() {
setTimeout(function(){
var newwidth = Math.pow(2, Math.floor(getBaseLog(2, $(window).width() * 0.88)));
fscanvas.width = newwidth;
}, 200);
});
var gl = fscanvas.getContext("webgl"/*, {antialias: true}*/);
const r3webgl = {...parentr3webgl};
r3webgl.gl = gl;
/*
Anti Aliasing intensity setting
gl.enable(gl.SAMPLE_COVERAGE);
gl.sampleCoverage(0.5, false);
*/
// --- ADD EVENT LISTENERS ---
$("#cellauto3dtoggle").click(toggle);
function toggle(){running ? (running = false, $("#cellauto3dtoggle").html("Start")) : (running = true, $("#cellauto3dtoggle").html("Stop"));}
$("#cellauto3dapply").click(apply);
function apply(){survivevalues = parseRulestring3D($("#cellRuleInput3D").val())[1]; bornvalues = parseRulestring3D($("#cellRuleInput3D").val())[0];}
$("#cellauto3drandomize").click(randomize);
function randomize(){cellgrid = createCellGridWireframe(cellularworldsize, true);}
// https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API
fscanvas.onclick = function(){
fscanvas.requestPointerLock();
}
var viewxz = 0;
var viewy = 0;
document.addEventListener("pointerlockchange", function(){
if (document.pointerLockElement === fscanvas) {
document.addEventListener("mousemove", updatePosition, false);
} else {
document.removeEventListener("mousemove", updatePosition, false);
}
}, false);
function updatePosition(e) {
viewxz -= e.movementX*0.1;
viewy = Math.min(90, Math.max(-90, viewy-e.movementY*0.1));
}
var keys = {};
$("#cellauto3dcanvas").keydown(function(event) {
keys[event.key] = true;
});
$("#cellauto3dcanvas").keyup(function(event) {
keys[event.key] = false;
});
// --- ---
// --- MAKE SHADERS AND PROGRAM ---
const program = r3webgl.createShaderProgram(vertexshadersource, fragmentshadersource);
gl.useProgram(program);
// --- GET ALL ATTRIBUTE AND UNIFORM LOCATIONS
const attribLocations = r3webgl.getAttribLocations(program, ["vertexposition", "texturecoordinate", "normal"]);
const uniformLocations = r3webgl.getUniformLocations(program, ["modelmatrix", "viewmatrix", "projectionmatrix", "texture", "reverseLightDirection"]);
// --- INIT 3D ---
r3webgl.init3D();
// --- THERE SHALL BE LIGHT ---
gl.uniform3fv(uniformLocations.reverseLightDirection, m4.normalize([1.0, 0.0, 0.0, 1.0]));
// GET DATA FROM OBJ
var cubevertexbuffer;
var cubetexcoordbuffer;
var cubenormalbuffer;
main();
async function main() {
const response = await fetch('webgl3dstudy/cube.obj');
const text = await response.text();
var data = parseOBJ(text, true);
console.log(data);
cubevertexbuffer = r3webgl.createBuffer(gl.ARRAY_BUFFER, data["Cube"].positions);
cubetexcoordbuffer = r3webgl.createBuffer(gl.ARRAY_BUFFER, data["Cube"].texcoords);
cubenormalbuffer = r3webgl.createBuffer(gl.ARRAY_BUFFER, data["Cube"].normals);
}
// --- GET OBJ TEXTURE ---
var texturecube = r3webgl.createTexture();
r3webgl.attachTextureSourceAsync(texturecube, "webgl3dstudy/cubetexturetest.png", true);
// --- ENABLE TEXTURE0 ---
gl.uniform1i(uniformLocations.texture, 0);
var camerapos = [30.0, 30.0, -100.0];
requestAnimationFrame(drawScene);
toggle();
var then = 0;
function drawScene(now){
if(running){
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// convert requestanimationframe timestamp to seconds
now *= 0.001;
// subtract the previous time from the current time
var deltaTime = now - then;
// remember the current time for the next frame
then = now;
// --- SETUP PROJECTION MATRIX --- (MAKE EVERYTHING 3D)
//var projectionmatrix = m4.createOrthographicMatrix(0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, 400, -400);
var projectionmatrix = m4.createPerspectiveMatrix(m4.degreeToRadians(46.0), gl.canvas.clientWidth/gl.canvas.clientHeight, 1, 200000);
gl.uniformMatrix4fv(uniformLocations.projectionmatrix, false, projectionmatrix);
// --- SETUP LOOKAT MATRIX ---
var lookatmatrix = m4.createIdentityMatrix();
lookatmatrix = m4.mult(m4.createTranslationMatrix(camerapos[0]+Math.sin(m4.degreeToRadians(viewxz)), camerapos[1]+Math.sin(m4.degreeToRadians(viewy)), camerapos[2]+Math.cos(m4.degreeToRadians(viewxz))), lookatmatrix);
var lookatposition = [lookatmatrix[12], lookatmatrix[13], lookatmatrix[14]];
// --- FIRST PERSON CAMERA ---
var movementspeed = 0.8;
var factorws = (keys["w"] ? 1 : keys["s"] ? -1 : 0);
lookatposition[0] += Math.sin(m4.degreeToRadians(viewxz))*movementspeed*factorws;
lookatposition[1] += Math.sin(m4.degreeToRadians(viewy))*movementspeed*factorws;
lookatposition[2] += Math.cos(m4.degreeToRadians(viewxz))*movementspeed*factorws;
camerapos[0] += Math.sin(m4.degreeToRadians(viewxz))*movementspeed*factorws;
camerapos[1] += Math.sin(m4.degreeToRadians(viewy))*movementspeed*factorws;
camerapos[2] += Math.cos(m4.degreeToRadians(viewxz))*movementspeed*factorws;
var factorad = (keys["d"] ? 1 : keys["a"] ? -1 : 0);
var movcamvector = m4.cross([Math.sin(m4.degreeToRadians(viewxz)), Math.sin(m4.degreeToRadians(viewy)), Math.cos(m4.degreeToRadians(viewxz))], [0,1,0]);
lookatposition[0] += movcamvector[0]*movementspeed*factorad;
lookatposition[2] += movcamvector[2]*movementspeed*factorad;
camerapos[0] += movcamvector[0]*movementspeed*factorad;
camerapos[2] += movcamvector[2]*movementspeed*factorad;
var factoreq = (keys["e"] ? movementspeed : keys["q"] ? -movementspeed : 0);
lookatposition[1] += factoreq;
camerapos[1] += factoreq;
// --- SETUP VIEWMATRIX --- (MOVE THE WORLD INVERSE OF THE CAMERAMOVEMENT)
var cameramatrix = m4.lookAt(camerapos, lookatposition, [0, 1, 0]);
var viewmatrix = m4.inverse(cameramatrix);
var viewmatrixlocation = gl.getUniformLocation(program, "viewmatrix");
gl.uniformMatrix4fv(uniformLocations.viewmatrix, false, viewmatrix);
// --- CONNECT BUFFERS TO ATTRIBUTES --- (only has to be done once since the only object vertex data we ever need is that of a cube)
r3webgl.connectBufferToAttribute(gl.ARRAY_BUFFER, cubevertexbuffer, attribLocations.vertexposition, 3, true);
r3webgl.connectBufferToAttribute(gl.ARRAY_BUFFER, cubenormalbuffer, attribLocations.normal, 3, true);
r3webgl.connectBufferToAttribute(gl.ARRAY_BUFFER, cubetexcoordbuffer, attribLocations.texturecoordinate, 2, true);
// -- DRAW ---
//console.time("drawloop");
for(var x = 0; x < cellularworldsize; x++){
for(var y = 0; y < cellularworldsize; y++){
for(var z = 0; z < cellularworldsize; z++){
if(cellgrid[x][y][z] == 1){
gl.uniformMatrix4fv(uniformLocations.modelmatrix, false, r3webgl.createModelMatrix(x, y, z, 0, 0, 0, 0.5, 0.5, 0.5));
gl.drawArrays(gl.TRIANGLES, 0, 6*2*3);
}
}
}
}
//console.timeEnd("drawloop");
}
requestAnimationFrame(drawScene);
}
}