Three.js: using RawShaderMaterial with LineSegments - javascript

I am working on a three.js scene that renders points and line segments. The scene renders fine if I use a LineBasicMaterial material for the lines:
/**
* constructor for the gl manager
**/
function World() {
this.renderTarget = document.querySelector('#render-target');
this.scene = this.getScene();
this.camera = this.getCamera();
this.renderer = this.getRenderer();
this.controls = this.getControls();
this.masterCounts = null; // {id: nMasters}
this.edges = null; // 2d array where [[master, app, app]]
this.positions = null; // {id: [x,y]}
this.z = 0; // flat z dim
this.loadData();
this.render();
}
World.prototype.getScene = function() {
return new THREE.Scene();
}
World.prototype.getContainerSize = function() {
var elem = this.renderTarget;
return {
w: elem.clientWidth,
h: elem.clientHeight,
}
}
World.prototype.getCamera = function() {
var size = this.getContainerSize();
var camera = new THREE.PerspectiveCamera(75, size.w/size.h, 0.01, 10);
camera.position.set(0.5, 0.5, -0.67);
return camera;
}
World.prototype.getRenderer = function() {
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
var size = this.getContainerSize();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(size.w, size.h);
document.querySelector('#render-target').appendChild(renderer.domElement);
return renderer;
}
World.prototype.getControls = function() {
var controls = new THREE.TrackballControls(this.camera, this.renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
controls.target.set(0.5, 0.5, 1);
return controls;
}
World.prototype.render = function() {
requestAnimationFrame(this.render.bind(this));
this.renderer.render(this.scene, this.camera);
this.controls.update();
}
World.prototype.getPointScale = function() {
return window.devicePixelRatio * window.innerHeight * 0.00001;
}
World.prototype.loadData = function() {
get('https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/node-positions-twopi.json', function(data) {
this.positions = center(JSON.parse(data));
get('https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/id-to-aggregate-masters.json', function(data) {
this.masterCounts = JSON.parse(data);
get('https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/edges.json', function(data) {
this.edges = JSON.parse(data);
this.addPoints();
this.addEdges();
}.bind(this));
}.bind(this));
}.bind(this));
}
World.prototype.addPoints = function() {
var geometry = new THREE.InstancedBufferGeometry(),
translations = getPointTranslations(this.positions),
colors = getColors(this.positions, this.masterCounts);
geometry.addAttribute('position',
new THREE.BufferAttribute( new Float32Array([0, 0, 0]), true, 3));
geometry.addAttribute('translation',
new THREE.InstancedBufferAttribute(translations, 3, true, 1) );
geometry.addAttribute('target',
new THREE.InstancedBufferAttribute(translations, 3, true, 1) );
geometry.addAttribute('color',
new THREE.InstancedBufferAttribute(colors, 3, true, 1) );
this.points = new THREE.Points(geometry, this.getShaderMaterial());
this.points.frustumCulled = false; // prevent mesh click on drag
this.scene.add(this.points);
}
World.prototype.addEdges = function() {
var indices = [],
positions = [],
idToIndex = {}, // {node id: index in edgePositions}
ids = Object.keys(this.edges);
// flatten edges into [[s,t],[s,t]]
for (var i=0; i<ids.length; i++) {
var idEdges = this.edges[ids[i]];
for (var j=0; j<idEdges.length; j++) {
// here ids[i] is a master node id, idEdges is list of
// apprentice node ids
var masterId = ids[i];
var apprenticeId = idEdges[j];
if (!(masterId in idToIndex)) {
idToIndex[masterId] = positions.length;
positions.push(this.positions[masterId]);
}
if (!(apprenticeId in idToIndex)) {
idToIndex[apprenticeId] = positions.length;
positions.push(this.positions[apprenticeId]);
}
indices = indices.concat([
idToIndex[masterId],
idToIndex[apprenticeId]
]);
}
}
var geometry = new THREE.BufferGeometry(),
translations = new Float32Array(3*positions.length),
iter = 0,
indices = new Uint16Array(indices);
for (var i=0; i<positions.length; i++) {
var e = positions[i];
translations[iter++] = e[0];
translations[iter++] = e[1];
translations[iter++] = this.z;
}
var material = new THREE.LineBasicMaterial({
color: 0xee6559,
opacity: 0.3,
transparent: true,
})
geometry.addAttribute('position',
new THREE.BufferAttribute(translations, 3, true, 1));
geometry.setIndex(new THREE.BufferAttribute(indices, 1, true, 1));
this.lines = new THREE.LineSegments(geometry, material);
this.lines.frustumCulled = false; // prevent mesh click on drag
this.scene.add(this.lines);
}
World.prototype.getShaderMaterial = function() {
return new THREE.RawShaderMaterial({
vertexShader: find('#vertex-shader').textContent,
fragmentShader: find('#fragment-shader').textContent,
uniforms: {
transitionPercent: { type: 'f', value: 0.0 },
pointScale: { type: 'f', value: this.getPointScale(), },
}
});
}
/**
* Helpers
**/
function get(url, handleSuccess, handleErr, handleProgress) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
if (xmlhttp.status === 200) {
if (handleSuccess) handleSuccess(xmlhttp.responseText)
} else {
if (handleErr) handleErr(xmlhttp)
}
};
};
xmlhttp.onprogress = function(e) {
if (handleProgress) handleProgress(e);
};
xmlhttp.open('GET', url, true);
xmlhttp.send();
};
function find(querySelector) {
return document.querySelector(querySelector);
}
window.addEventListener('resize', function() {
var size = world.getContainerSize();
world.camera.aspect = size.w / size.h;
world.camera.updateProjectionMatrix();
world.renderer.setSize(size.w, size.h);
})
function center(data) {
var ids = Object.keys(data);
// find the domains of each axis in the data
var p = Number.POSITIVE_INFINITY,
n = Number.NEGATIVE_INFINITY,
domains = {x: {min: p, max: n}, y: {min: p, max: n}};
for (var i=0; i<ids.length; i++) {
vals = data[ids[i]];
if (vals[0] < domains.x.min) domains.x.min = vals[0];
if (vals[0] > domains.x.max) domains.x.max = vals[0];
if (vals[1] < domains.y.min) domains.y.min = vals[1];
if (vals[1] > domains.y.max) domains.y.max = vals[1];
}
// center each axis 0:1
for (var i=0; i<ids.length; i++) {
var d = data[ids[i]];
d[0] = (d[0]-domains.x.min)/(domains.x.max-domains.x.min);
d[1] = (d[1]-domains.y.min)/(domains.y.max-domains.y.min);
}
return data;
}
function getPointTranslations(data) {
var ids = Object.keys(data);
var arr = new Float32Array(ids.length*3),
iter = 0;
for (var i=0; i<ids.length; i++) {
arr[iter++] = data[ids[i]][0];
arr[iter++] = data[ids[i]][1];
arr[iter++] = world.z;
}
return arr;
}
function getColors(data, masterCounts) {
var ids = Object.keys(data);
var maxMasters = 0;
for (var i=0; i<ids.length; i++) {
if (masterCounts[ids[i]] > maxMasters) maxMasters = masterCounts[ids[i]];
}
var colors = [
'#1f77b4', '#86abd7', '#cbcdd3', '#f8dba8',
'#eec570', '#eba055', '#ee6559',
];
var arr = new Float32Array(ids.length * 3),
iter = 0;
for (var i=0; i<ids.length; i++) {
var nMasters = Math.min(colors.length, masterCounts[ids[i]] || 0);
var hex = colors[ Math.floor(colors.length * (nMasters / maxMasters)) ];
var c = hexToRgb(hex);
arr[iter++] = c.r / 255;
arr[iter++] = c.g / 255;
arr[iter++] = c.b / 255;
}
return arr;
}
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? '0' + hex : hex;
}
function rgbToHex(r, g, b) {
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : {r: 0, g: 0, b: 0};
}
var world = new World();
html,
body {
width: 100%;
height: 100%;
background: linear-gradient(#efefef, #efefef);
}
body {
margin: 0;
overflow: hidden;
}
div#select-target {
padding: 20px 0;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
font-family: 'Mallory';
text-indent: 0.01px;
text-overflow: '';
border: none;
padding: 7px 40px 7px 10px;
background-image: url(down-caret.png);
background-position: 90% 50%;
background-size: 15px;
background-repeat: no-repeat;
font-size: 1em;
text-transform: uppercase;
border: 1px solid #c7c7c7;
font-family: arial, sans-serif;
}
#render-container {
text-align: center;
max-height: 100%;
max-width: 100%;
padding-bottom: 20px;
}
#render-target {
margin: 0 auto;
width: 700px;
height: 700px;
}
<div id='render-container'>
<div id='select-target'></div>
<div id='render-target'></div>
</div>
<script src='https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/three.min.js'></script>
<script src='https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/trackball-controls.min.js'></script>
<script src='https://s3.amazonaws.com/duhaime/blog/visualizations/line-segments-network/tweenlite.min.js'></script>
<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float transitionPercent;
uniform vec3 cameraPosition;
uniform float pointScale;
attribute vec3 position;
attribute vec3 translation;
attribute vec3 target;
attribute vec3 color;
varying vec3 vColor;
float scalePointZ(in vec4 pos, in vec3 cameraPosition) {
float zDelta = pow(pos[2] - cameraPosition[2], 2.0);
float delta = pow(zDelta, 0.5);
float scaled = pointScale / delta;
return scaled;
}
void main() {
vec3 t1 = position + translation;
vec3 t2 = position + target;
vec3 pos = mix(t1, t2, clamp(transitionPercent, 0.0, 1.0));
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPos;
gl_PointSize = 6.0;
vColor = color;
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;
varying vec3 vColor;
void main() {
// make points circular
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
// set point color
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
</script>
I have been struggling to achieve the same scene using the rawshadermaterial for the lines, however. Here's one of the ways I've tried to set up the lines for the raw shader material:
World.prototype.addEdges = function() {
var indices = [],
positions = [],
idToIndex = {}, // {node id: index in edgePositions}
ids = Object.keys(this.edges);
// flatten edges into [[s,t],[s,t]]
for (var i=0; i<ids.length; i++) {
var idEdges = this.edges[ids[i]];
for (var j=0; j<idEdges.length; j++) {
// here ids[i] is a master node id, idEdges is list of
// apprentice node ids
var masterId = ids[i];
var apprenticeId = idEdges[j];
if (!(masterId in idToIndex)) {
idToIndex[masterId] = positions.length;
positions.push(this.positions[masterId]);
}
if (!(apprenticeId in idToIndex)) {
idToIndex[apprenticeId] = positions.length;
positions.push(this.positions[apprenticeId]);
}
indices = indices.concat([
idToIndex[masterId],
idToIndex[apprenticeId]
]);
}
}
var geometry = new THREE.BufferGeometry(),
translations = new Float32Array(3*positions.length),
iter = 0,
indices = new Uint16Array(indices);
for (var i=0; i<positions.length; i++) {
var e = positions[i];
translations[iter++] = e[0];
translations[iter++] = e[1];
translations[iter++] = this.z;
}
var material = this.getShaderMaterial().clone();
geometry.addAttribute('position',
new THREE.BufferAttribute(new Float32Array([0, 0, 0]), 3, true, 1));
geometry.addAttribute('translation',
new THREE.BufferAttribute(translations, 3, true, 1));
geometry.addAttribute('target',
new THREE.BufferAttribute(translations, 3, true, 1));
geometry.setIndex(new THREE.BufferAttribute(indices, 1, true, 1));
this.lines = new THREE.LineSegments(geometry, material);
this.lines.frustumCulled = false; // prevent mesh click on drag
this.scene.add(this.lines);
}
However this renders nothing. Does anyone know how I can render the lines above with the raw shader material defined above? Any pointers or suggestions would be very helpful!

I think this happens because you are using gl_PointCoord in the fragment shader although you are not rendering points but lines. If I remove the following two lines of code, your lines are rendered:
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
Demo: https://jsfiddle.net/Ldynhxkq/
Maybe it's better to use different shader programs for both primitives.

Related

Threejs / EffectComposer - Interactively masking an UnrealBloomPass

So, I am trying to dynamically mask an UnrealBloomPass with the EffectComposer and I am getting unexpected results. I am not sure if I am missing a key concept here, or if I should be trying to achieve this in a different way. Any input would be appreciated.
The composer is set up into these layers:
hexgradientPass (Main content)
maskingPass (I am trying to move this dynamically with the mouse)
bloomPass (Which I am trying to mask with ^)
clearMaskPass (Clearing the mask)
effectCopyPass (Applying the effects)
I've been following tutorials and examples as closely as possible and still no dice. Thanks in advance.
<!DOCTYPE html>
<html lang="en">
<head>
<title>BloomShader Mask</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
color: #fff;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 5px;
}
#info p {
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0 2em;
}
a {
color: #2983ff;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="./build/three.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/libs/dat.gui.min.js"></script>
<script src="js/controls/OrbitControls.js"></script>
<script src="js/loaders/GLTFLoader.js"></script>
<script src="js/postprocessing/EffectComposer.js"></script>
<script src="js/postprocessing/RenderPass.js"></script>
<script src="js/postprocessing/ShaderPass.js"></script>
<script src="js/postprocessing/MaskPass.js "></script>
<script src="js/shaders/CopyShader.js"></script>
<script src="js/shaders/LuminosityHighPassShader.js"></script>
<script src="js/postprocessing/UnrealBloomPass.js"></script>
<script id="gradientShader" type="shader-code">
uniform vec2 resolution;
uniform vec2 mousePosition;
void main() {
vec2 pos = (gl_FragCoord.xy - mousePosition.xy * 1.2) / resolution.xy;
gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
}
</script>
<script>
var clock = new THREE.Clock();
var stats;
var gui;
var sceneHexGradient;
var sceneMask;
var camera;
var orbitControls;
var pointLight;
var composer;
var mixer;
var maskerMesh;
var bloomPass;
var width = 0;
var height = 0;
var cameraZoom = 120;
var mousePositionY = 0;
var params = {
exposure: 2,
bloomStrength: .81,
bloomThreshold: 0,
bloomRadius: .05,
cameraZoom: cameraZoom
};
function init() {
// Init vars (DOM needed)
gui = initGui();
stats = initStats();
width = window.innerWidth;
height = window.innerHeight;
pixelRatio = window.devicePixelRatio;
// Renderer
var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setPixelRatio(pixelRatio);
webGLRenderer.setSize(width , height);
webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
webGLRenderer.shadowMap.enabled = true;
appendChild(webGLRenderer.domElement);
// Scene
sceneHexGradient = new THREE.Scene();
sceneMasker = new THREE.Scene();
// Camera
camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
camera.zoom = cameraZoom;
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera.updateProjectionMatrix();
sceneHexGradient.add(camera);
sceneMasker.add(camera);
// Orbit
orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
orbitControls.maxPolarAngle = Math.PI * 0.5;
orbitControls.minDistance = 1;
orbitControls.maxDistance = 10;
// Draw onto Scenes
drawGradientToScene(sceneHexGradient);
drawHexToScene(sceneHexGradient);
drawMaskerToScene(sceneMasker);
// ShaderPass
var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
hexgradientPass.clear = false;
var effectCopyPass = new THREE.ShaderPass(THREE.CopyShader);
effectCopyPass.renderToScreen = true;
var maskingPass = new THREE.MaskPass(sceneMasker, camera);
maskingPass.clear = true;
// var maskingPass = new THREE.RenderPass(sceneMasker, camera);
// maskingPass.renderToScreen = true;
bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
bloomPass.renderToScreen = true;
bloomPass.threshold = params.bloomThreshold;
bloomPass.strength = params.bloomStrength;
bloomPass.radius = params.bloomRadius;
var clearMaskPass = new THREE.ClearMaskPass();
composer = new THREE.EffectComposer(webGLRenderer);
composer.renderTarget1.stencilBuffer = true;
composer.renderTarget2.stencilBuffer = true;
composer.setSize(width, height);
composer.addPass(hexgradientPass);
composer.addPass(maskingPass);
// composer.addPass(maskingVerticalBlurPass);
composer.addPass(bloomPass);
composer.addPass(clearMaskPass);
composer.addPass(effectCopyPass);
render();
function initStats() { // Debug only
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
appendChild(stats.dom);
return stats;
}
function initGui() { // Debug only
gui = new dat.GUI();
gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
});
gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
bloomPass.threshold = Number(value);
});
gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
bloomPass.strength = Number(value);
});
gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
bloomPass.radius = Number(value);
});
gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
camera.zoom = Number(value);
camera.updateProjectionMatrix();
});
return gui;
}
function appendChild(domElement) {
var container = document.getElementById('container');
if (container) {
return container.appendChild(domElement);
}
return false;
}
function drawGradientToScene(scene) {
var gradientUniforms = {};
gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
var shaderCode = document.getElementById('gradientShader').innerHTML;
var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
var geometry = new THREE.PlaneBufferGeometry(width, height);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
function drawHexToScene(scene) {
var hexes = [];
var colCount = 100;
var rowCount = 100;
var hexDiameter = 1;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
var yStart = (rowCount - 1) * (rowSpace * 0.5);
var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: false,
}));
var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
var y = (yStart - j * rowSpace);
hex.position.set(x, y, 0);
hexes.push(hex);
scene.add(hex);
}
}
}
function drawMaskerToScene(scene) {
var boxGeometry = new THREE.BoxGeometry(width, height, 10);
var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
maskerMesh = new THREE.Mesh(boxGeometry, basicMaterial);
maskerMesh.position.y = -height;
scene.add(maskerMesh);
}
function onResze() {
camera.position.set(0, 0, -15);
camera.zoom = cameraZoom;
camera.updateProjectionMatrix();
composer.setSize(width, height);
}
function onMouseMove(event) {
mousePositionY = event.clientY;
}
function render() {
webGLRenderer.autoClear = false;
stats.update();
var delta = clock.getDelta();
// orbitControls.update(delta);
requestAnimationFrame(render);
maskerMesh.position.y = mousePositionY;
// composer.setSize(window.innerWidth, window.innerHeight);
composer.render();
}
window.addEventListener('mousemove', onMouseMove);
} // End - init
// Listeners
window.onload = init;
window.onresize = init.onResze;
</script>
</body>
</html>
It was actually a combination of issues. The main issue was setting the flags on the passes, to clear or to render to screen. This wasn't an easy thing to debug:
<!DOCTYPE html>
<html lang="en">
<head>
<title>BloomShader Mask</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no,
minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
color: #fff;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 5px;
}
#info p {
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0 2em;
}
a {
color: #2983ff;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="./build/three.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/libs/dat.gui.min.js"></script>
<script src="js/controls/OrbitControls.js"></script>
<script src="js/loaders/GLTFLoader.js"></script>
<script src="js/postprocessing/EffectComposer.js"></script>
<script src="js/postprocessing/ClearPass.js"></script>
<script src="js/postprocessing/RenderPass.js"></script>
<script src="js/postprocessing/ShaderPass.js"></script>
<script src="js/postprocessing/MaskPass.js "></script>
<script src="js/shaders/CopyShader.js"></script>
<script src="js/shaders/LuminosityHighPassShader.js"></script>
<script src="js/postprocessing/UnrealBloomPass.js"></script>
<script id="gradientShader" type="shader-code">
uniform vec2 resolution;
uniform vec2 mousePosition;
void main() {
vec2 pos = ((gl_FragCoord.xy - mousePosition.xy * 1.3) / resolution.xy);
gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
}
</script>
<script>
var clock = new THREE.Clock();
var stats;
var gui;
var sceneHexGradient;
var sceneBloomMasker;
var sceneMasker;
var camera;
var orbitControls;
var pointLight;
var composer;
var mixer;
var maskerMesh;
var bloomPass;
var width = 0;
var height = 0;
var cameraZoom = 120;
var mousePositionY = 0;
var params = {
exposure: 2, //1.1
bloomStrength: 1.75,
bloomThreshold: 0,
bloomRadius: .16,
cameraZoom: cameraZoom
};
function init() {
// Init vars (DOM needed)
gui = initGui();
stats = initStats();
width = window.innerWidth;
height = window.innerHeight;
pixelRatio = window.devicePixelRatio;
// Camera
camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
camera.zoom = cameraZoom;
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera.updateProjectionMatrix();
// Renderer
var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setPixelRatio(pixelRatio);
webGLRenderer.setSize(width , height);
webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
webGLRenderer.shadowMap.enabled = true;
webGLRenderer.autoClear = false;
appendChild(webGLRenderer.domElement);
// Scene
sceneHexGradient = new THREE.Scene();
sceneBloom = new THREE.Scene();
sceneMasker = new THREE.Scene();
sceneHexGradient.add(camera);
sceneBloom.add(camera);
sceneMasker.add(camera);
// Lights (are they needed ? )
sceneBloom.add(new THREE.AmbientLight(0xffffff));
// Orbit
orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
orbitControls.maxPolarAngle = Math.PI * 0.5;
orbitControls.minDistance = 1;
orbitControls.maxDistance = 10;
// Draw onto Scenes
drawGradientToScene(sceneHexGradient);
drawHexToScene(sceneHexGradient);
drawMaskerToScene(sceneMasker);
bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
bloomPass.threshold = params.bloomThreshold;
bloomPass.strength = params.bloomStrength;
bloomPass.radius = params.bloomRadius;
// ShaderPasses
var clearPass = new THREE.ClearPass();
var clearMaskPass = new THREE.ClearMaskPass();
var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
var maskingPass = new THREE.MaskPass(sceneMasker, camera);
var outputPass = new THREE.ShaderPass(THREE.CopyShader);
outputPass.renderToScreen = true;
var parameters = {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat,
stencilBuffer: true
};
var renderTarget = new THREE.WebGLRenderTarget(width, height, parameters);
composer = new THREE.EffectComposer(webGLRenderer, renderTarget);
composer.addPass(clearPass);
composer.addPass(hexgradientPass);
composer.addPass(maskingPass);
composer.addPass(bloomPass);
composer.addPass(clearMaskPass);
composer.addPass(outputPass);
function initStats() { // Debug only
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
appendChild(stats.dom);
return stats;
}
function initGui() { // Debug only
gui = new dat.GUI();
gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
});
gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
bloomPass.threshold = Number(value);
});
gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
bloomPass.strength = Number(value);
});
gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
bloomPass.radius = Number(value);
});
gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
camera.zoom = Number(value);
camera.updateProjectionMatrix();
});
return gui;
}
function appendChild(domElement) {
var container = document.getElementById('container');
if (container) {
return container.appendChild(domElement);
}
return false;
}
function getRandomNumber(min = 1, max = 100) {
return Math.floor(Math.random() * (+max - +min)) + +min;
}
function getArrayOfRandomNumbers(count = 1, min = 1, max = 100) {
const outArr = [];
for( i = 0; i < count; ++i) {
outArr.push(getRandomNumber(min, max));
}
return outArr.sort();
}
function drawBlackoutMask(scene) {
// var x = 0, y = 0;
// var hexDiameter = 2;
// var shapeWidth = 10;
// var facets = getRandomNumber(1, shapeWidth);
// var xValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
// var yValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
// var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
// hexGeom.rotateX(Math.PI * 0.5);
// var boxGeometry = new THREE.BoxGeometry(10, 10, 10);
// var mesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({ color: 0xff0000 }));
// mesh.position.x = 0;
// mesh.position.y = 0;
// scene.add(mesh);
// for(var i = 0; i < facets; i++) {
// var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
// var mesh = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
// color: 0xff0000,
// wireframe: false,
// }));
// mesh.position.x = xValuesArr[i];
// mesh.position.y = yValuesArr[i];
// scene.add(mesh);
// }
}
function drawMaskerToScene(scene) {
var boxGeometry = new THREE.CylinderBufferGeometry(width, height, 10);
// var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
boxGeometry.computeBoundingBox();
var material = new THREE.ShaderMaterial({
uniforms: {
color1: {
value: new THREE.Color(0xffffff)
},
color2: {
value: new THREE.Color(0x000000)
},
bboxMin: {
value: boxGeometry.boundingBox.min
},
bboxMax: {
value: boxGeometry.boundingBox.max
}
},
vertexShader: `
uniform vec3 bboxMin;
uniform vec3 bboxMax;
varying vec2 vUv;
void main() {
vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color1;
uniform vec3 color2;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
}
`,
wireframe: false
});
maskerMesh = new THREE.Mesh(boxGeometry, material); //basicMaterial);
// maskerMesh.scale.set(1,1,1);
// maskerMesh.position.y = -height;
scene.add(maskerMesh);
}
function drawGradientToScene(scene) {
var gradientUniforms = {};
gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
var shaderCode = document.getElementById('gradientShader').innerHTML;
var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
var geometry = new THREE.PlaneBufferGeometry(width, height);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
function drawHexToScene(scene) {
var hexes = [];
var colCount = 100;
var rowCount = 100;
var hexDiameter = 1;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
var yStart = (rowCount - 1) * (rowSpace * 0.5);
var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: false,
}));
var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
var y = (yStart - j * rowSpace);
hex.position.set(x, y, 0);
hexes.push(hex);
scene.add(hex);
}
}
}
function onResze() {
camera.position.set(0, 0, -15);
camera.zoom = cameraZoom;
camera.updateProjectionMatrix();
composer.setSize(width, height);
}
function onMouseMove(event) {
mousePositionY = event.clientY;
}
function render() {
webGLRenderer.autoClear = false;
stats.update();
var delta = clock.getDelta();
// orbitControls.update(delta);
requestAnimationFrame(render);
maskerMesh.position.y = mousePositionY;
webGLRenderer.clear();
composer.render(delta);
}
window.addEventListener('mousemove', onMouseMove);
render();
} // End - init
// Listeners
window.onload = init;
window.onresize = init.onResze;
</script>
</body>

THREE.js - passing attributes to BufferGeometry instead of ShaderMaterial?

I'm trying to get a THREE.js example for version 58 to run on a current version of THREE.js - here's the original example.
There were a few errors I was able to get rid of by just commenting stuff out, but one trickier error is this:
THREE.ShaderMaterial: attributes should now be defined in THREE.BufferGeometry instead.
THREE.ShaderMaterial: 'attributes' is not a property of this material.
The offending code is this:
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: uniforms,
attributes: attributes,
vertexShader: document.getElementById( 'vertexshader_lines' ).textContent,
fragmentShader: document.getElementById( 'fragmentshader_lines' ).textContent,
});
Where attributes is defined a few lines above as:
var attributes = {
draw: { type: 'f', value: [] },
seed: { type: 'f', value: [] },
seed2: { type: 'f', value: [] },
customColor: { type: 'c', value: [] },
index2: { type: 'f', value: [] },
norm: { type: 'v3', value: [] },
};
I tried to fix this by commenting out the attributes parameter passed to the THREE.ShaderMaterial constructor, and instead converting the THREE.Geometry from the original version (named lineGeo) into a THREE.BufferedGeometry and then passing in the attributes using its addAttribute method, like so:
var bufferLineGeo = new THREE.BufferGeometry();
bufferLineGeo.fromGeometry(lineGeo);
var values_bColor = new Float32Array(values_color.length * 3);
var values_bNorm = new Float32Array(values_norm.length * 3);
bufferLineGeo.addAttribute('draw', new THREE.BufferAttribute(new Float32Array(values_draw), 1));
bufferLineGeo.addAttribute('seed', new THREE.BufferAttribute(new Float32Array(values_seed), 1));
bufferLineGeo.addAttribute('seed2', new THREE.BufferAttribute(new Float32Array(values_seed2), 1));
bufferLineGeo.addAttribute('customColor', new THREE.BufferAttribute(values_bColor, 3).copyColorsArray( values_color));
bufferLineGeo.addAttribute('index2', new THREE.BufferAttribute(new Float32Array(values_index2), 1));
bufferLineGeo.addAttribute('norm', new THREE.BufferAttribute(values_bNorm, 3).copyVector3sArray(values_norm));
This didn't work and I'm at a dead end now for what to try next. It renders nothing - I'm left with a black screen and nothing in the console to tell me what went wrong.
Minimal, Complete Example of it not working with current versions of THREE.js
<!doctype html>
<html lang="en">
<head>
<title>Long hair</title>
<style type="text/css">
body {
background:#000000;
}
</style>
</head>
<body>
<!--<script src="../build_r58/three.min.js"></script>-->
<script src="../js/three.min.js"></script>
<script type="x-shader/x-vertex" id="vertexshader_lines">
uniform float globalTime;
uniform vec3 gravity;
uniform vec3 gravity2;
uniform float spacing;
attribute vec3 customColor;
attribute float seed;
attribute float seed2;
attribute float draw;
attribute float index2;
attribute vec3 norm;
varying vec3 vColor;
varying float vDraw;
varying vec3 vNormal;
void main() {
vDraw = draw;
vColor = customColor;
vec3 displacement = vec3(0.0,0.0,0.0);
vec3 forceDirection = vec3(0.0,0.0,0.0);
float displacementFactor = pow(index2, 1.2);
float displacementFactor2 = pow(index2, 2.5);
float displacementFactor3 = pow(1.0-index2, 1.0);
// "gravity"
vec3 g = gravity;
g.x *= displacementFactor2*seed2;
// "wind"
forceDirection.x = sin(globalTime*0.1+seed2*5.0+index2*1.0) * 0.1*displacementFactor;
forceDirection.y = cos(globalTime*0.7+seed2*5.0+index2*1.0) * 0.1*displacementFactor3;
forceDirection.z = sin(globalTime*0.7+seed2*5.0+index2*4.0) * 0.1*displacementFactor2;
displacement = g + forceDirection + ((1.0-index2)*gravity2)*seed;
vec3 aNormal = norm;
aNormal.xyz += displacement*displacementFactor;
vNormal = norm*(1.0-index2);
vNormal += (gravity2-gravity)*0.05;
vec3 animated = position;
// curl it slightly
animated.x += aNormal.x*index2*30.0*displacementFactor3;
animated += aNormal*index2*(spacing*seed);
if (animated.y < -150.0+seed2*20.0) {
animated.y = -150.0+seed2*20.0;
vDraw = 0.0;
}
vec4 mvPosition = modelViewMatrix * vec4( animated, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader_lines">
uniform vec3 color;
varying vec3 vColor;
varying float vDraw;
varying vec3 vNormal;
void main() {
if (vDraw == 0.0) {
discard;
}
float depth = gl_FragCoord.z / gl_FragCoord.w;
float fogFactor = smoothstep( 450.0, 300.0, depth );
// light
vec3 light = vec3(0.5,1.0,0.8);
float d = pow(max(0.25,dot(vNormal.xyz, light))*2.0, 1.5);
gl_FragColor = vec4( (color * vColor) * d * fogFactor, 1.0 );
}
</script>
<script>
var camera, scene, renderer;
var delta;
var time;
var oldTime;
var uniforms;
var hair;
var gravity = new THREE.Vector3(0,-5,0);
var gravity2 = new THREE.Vector3(0,-5,0);
init();
render();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.z = 400;
camera.lookAt(scene.position);
scene.add( camera );
var attributes = {
draw: { type: 'f', value: [] },
seed: { type: 'f', value: [] },
seed2: { type: 'f', value: [] },
customColor: { type: 'c', value: [] },
index2: { type: 'f', value: [] },
norm: { type: 'v3', value: [] },
};
uniforms = {
color: { type: "c", value: new THREE.Color( 0xe4b67b ) },
globalTime: { type: "f", value: 0.0 },
gravity: { type: "v3", value: gravity },
gravity2: { type: "v3", value: gravity2 },
spacing: { type: "f", value: 25.0 },
};
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: uniforms,
//attributes: attributes,
vertexShader: document.getElementById( 'vertexshader_lines' ).textContent,
fragmentShader: document.getElementById( 'fragmentshader_lines' ).textContent,
});
shaderMaterial.linewidth = 1;
var lineGeo = new THREE.Geometry();
var radius = 15;
var num = 70;
var baseGeo = new THREE.SphereGeometry(radius, num, num, undefined, undefined, 0.2, Math.PI*0.8);
for (var i = 0; i < baseGeo.vertices.length; i++) {
baseGeo.vertices[i].x += Math.random()*4-2;
baseGeo.vertices[i].y += Math.random()*4-2;
baseGeo.vertices[i].z += Math.random()*4-2;
}
var seedArray = [];
var seedArray2 = [];
var colorArray = [];
var drawArray = [];
var index2Array = [];
var normArray = [];
for (var i = 0; i < baseGeo.vertices.length; i++) {
var num = 30;
var base = baseGeo.vertices[i];
var seed = 1+Math.random()*0.5;
var seed2 = 0.25 + Math.random()*0.75;
var norm = new THREE.Vector3().copy(base).normalize();
norm = norm.normalize();
var black = 0.65+Math.random()*0.75;
for (var j = 0; j < num; j++) {
var vertex = new THREE.Vector3().copy(base);
var color = new THREE.Color(0xffffff);
color.setRGB(1.0*black,1.0*black,1.0*black);
lineGeo.vertices.push( vertex );
colorArray.push( color );
seedArray.push( seed );
seedArray2.push( seed2 );
index2Array.push( j/num );
normArray.push( norm );
if (j == num-1 || j == 0) {
drawArray.push( 0 );
} else {
drawArray.push( 1 );
}
}
}
var vertices = lineGeo.vertices;
var values_color = attributes.customColor.value;
var values_seed = attributes.seed.value;
var values_seed2 = attributes.seed2.value;
var values_draw = attributes.draw.value;
var values_index2 = attributes.index2.value;
var values_norm = attributes.norm.value;
for( var v = 0; v < vertices.length; v++ ) {
values_seed[ v ] = seedArray[v];
values_seed2[ v ] = seedArray2[v];
values_draw[ v ] = drawArray[v];
values_color[ v ] = colorArray[v];
values_index2[ v ] = index2Array[v];
values_norm[ v ] = normArray[v];
}
var bufferLineGeo = new THREE.BufferGeometry();
bufferLineGeo.fromGeometry(lineGeo);
var values_bColor = new Float32Array(values_color.length * 3);
var values_bNorm = new Float32Array(values_norm.length * 3);
bufferLineGeo.addAttribute('draw', new THREE.BufferAttribute(new Float32Array(values_draw), 1));
bufferLineGeo.addAttribute('seed', new THREE.BufferAttribute(new Float32Array(values_seed), 1));
bufferLineGeo.addAttribute('seed2', new THREE.BufferAttribute(new Float32Array(values_seed2), 1));
bufferLineGeo.addAttribute('customColor', new THREE.BufferAttribute(values_bColor, 3).copyColorsArray( values_color));
bufferLineGeo.addAttribute('index2', new THREE.BufferAttribute(new Float32Array(values_index2), 1));
bufferLineGeo.addAttribute('norm', new THREE.BufferAttribute(values_bNorm, 3).copyVector3sArray(values_norm));
hair = new THREE.Line(bufferLineGeo,
//lineGeo,
shaderMaterial, THREE.LineStrip );
scene.add(hair);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function render() {
requestAnimationFrame(render);
time = new Date().getTime();
delta = time - oldTime;
oldTime = time;
if (isNaN(delta) || delta > 1000 || delta == 0 ) {
delta = 1000/60;
}
uniforms.globalTime.value += delta * 0.005;
renderer.render( scene, camera );
}
</script>
</body>
</html>
Nearly Identical code as above, but successfully runs on version 58 of THREE.js
<!doctype html>
<html lang="en">
<head>
<title>Long hair</title>
<style type="text/css">
body {
background:#000000;
}
</style>
</head>
<body>
<script src="../build_r58/three.min.js"></script>
<!--<script src="../js/three.min.js"></script>-->
<script type="x-shader/x-vertex" id="vertexshader_lines">
uniform float globalTime;
uniform vec3 gravity;
uniform vec3 gravity2;
uniform float spacing;
attribute vec3 customColor;
attribute float seed;
attribute float seed2;
attribute float draw;
attribute float index2;
attribute vec3 norm;
varying vec3 vColor;
varying float vDraw;
varying vec3 vNormal;
void main() {
vDraw = draw;
vColor = customColor;
vec3 displacement = vec3(0.0,0.0,0.0);
vec3 forceDirection = vec3(0.0,0.0,0.0);
float displacementFactor = pow(index2, 1.2);
float displacementFactor2 = pow(index2, 2.5);
float displacementFactor3 = pow(1.0-index2, 1.0);
// "gravity"
vec3 g = gravity;
g.x *= displacementFactor2*seed2;
// "wind"
forceDirection.x = sin(globalTime*0.1+seed2*5.0+index2*1.0) * 0.1*displacementFactor;
forceDirection.y = cos(globalTime*0.7+seed2*5.0+index2*1.0) * 0.1*displacementFactor3;
forceDirection.z = sin(globalTime*0.7+seed2*5.0+index2*4.0) * 0.1*displacementFactor2;
displacement = g + forceDirection + ((1.0-index2)*gravity2)*seed;
vec3 aNormal = norm;
aNormal.xyz += displacement*displacementFactor;
vNormal = norm*(1.0-index2);
vNormal += (gravity2-gravity)*0.05;
vec3 animated = position;
// curl it slightly
animated.x += aNormal.x*index2*30.0*displacementFactor3;
animated += aNormal*index2*(spacing*seed);
if (animated.y < -150.0+seed2*20.0) {
animated.y = -150.0+seed2*20.0;
vDraw = 0.0;
}
vec4 mvPosition = modelViewMatrix * vec4( animated, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader_lines">
uniform vec3 color;
varying vec3 vColor;
varying float vDraw;
varying vec3 vNormal;
void main() {
if (vDraw == 0.0) {
discard;
}
float depth = gl_FragCoord.z / gl_FragCoord.w;
float fogFactor = smoothstep( 450.0, 300.0, depth );
// light
vec3 light = vec3(0.5,1.0,0.8);
float d = pow(max(0.25,dot(vNormal.xyz, light))*2.0, 1.5);
gl_FragColor = vec4( (color * vColor) * d * fogFactor, 1.0 );
}
</script>
<script>
var camera, scene, renderer;
var delta;
var time;
var oldTime;
var uniforms;
var hair;
var gravity = new THREE.Vector3(0,-5,0);
var gravity2 = new THREE.Vector3(0,-5,0);
init();
render();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.z = 400;
camera.lookAt(scene.position);
scene.add( camera );
var attributes = {
draw: { type: 'f', value: [] },
seed: { type: 'f', value: [] },
seed2: { type: 'f', value: [] },
customColor: { type: 'c', value: [] },
index2: { type: 'f', value: [] },
norm: { type: 'v3', value: [] },
};
uniforms = {
color: { type: "c", value: new THREE.Color( 0xe4b67b ) },
globalTime: { type: "f", value: 0.0 },
gravity: { type: "v3", value: gravity },
gravity2: { type: "v3", value: gravity2 },
spacing: { type: "f", value: 25.0 },
};
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: uniforms,
attributes: attributes,
vertexShader: document.getElementById( 'vertexshader_lines' ).textContent,
fragmentShader: document.getElementById( 'fragmentshader_lines' ).textContent,
});
shaderMaterial.linewidth = 1;
var lineGeo = new THREE.Geometry();
var radius = 15;
var num = 70;
var baseGeo = new THREE.SphereGeometry(radius, num, num, undefined, undefined, 0.2, Math.PI*0.8);
for (var i = 0; i < baseGeo.vertices.length; i++) {
baseGeo.vertices[i].x += Math.random()*4-2;
baseGeo.vertices[i].y += Math.random()*4-2;
baseGeo.vertices[i].z += Math.random()*4-2;
}
var seedArray = [];
var seedArray2 = [];
var colorArray = [];
var drawArray = [];
var index2Array = [];
var normArray = [];
for (var i = 0; i < baseGeo.vertices.length; i++) {
var num = 30;
var base = baseGeo.vertices[i];
var seed = 1+Math.random()*0.5;
var seed2 = 0.25 + Math.random()*0.75;
var norm = new THREE.Vector3().copy(base).normalize();
norm = norm.normalize();
var black = 0.65+Math.random()*0.75;
for (var j = 0; j < num; j++) {
var vertex = new THREE.Vector3().copy(base);
var color = new THREE.Color(0xffffff);
color.setRGB(1.0*black,1.0*black,1.0*black);
lineGeo.vertices.push( vertex );
colorArray.push( color );
seedArray.push( seed );
seedArray2.push( seed2 );
index2Array.push( j/num );
normArray.push( norm );
if (j == num-1 || j == 0) {
drawArray.push( 0 );
} else {
drawArray.push( 1 );
}
}
}
var vertices = lineGeo.vertices;
var values_color = attributes.customColor.value;
var values_seed = attributes.seed.value;
var values_seed2 = attributes.seed2.value;
var values_draw = attributes.draw.value;
var values_index2 = attributes.index2.value;
var values_norm = attributes.norm.value;
for( var v = 0; v < vertices.length; v++ ) {
values_seed[ v ] = seedArray[v];
values_seed2[ v ] = seedArray2[v];
values_draw[ v ] = drawArray[v];
values_color[ v ] = colorArray[v];
values_index2[ v ] = index2Array[v];
values_norm[ v ] = normArray[v];
}
/*var bufferLineGeo = new THREE.BufferGeometry();
bufferLineGeo.fromGeometry(lineGeo);
var values_bColor = new Float32Array(values_color.length * 3);
var values_bNorm = new Float32Array(values_norm.length * 3);
bufferLineGeo.addAttribute('draw', new THREE.BufferAttribute(new Float32Array(values_draw), 1));
bufferLineGeo.addAttribute('seed', new THREE.BufferAttribute(new Float32Array(values_seed), 1));
bufferLineGeo.addAttribute('seed2', new THREE.BufferAttribute(new Float32Array(values_seed2), 1));
bufferLineGeo.addAttribute('customColor', new THREE.BufferAttribute(values_bColor, 3).copyColorsArray( values_color));
bufferLineGeo.addAttribute('index2', new THREE.BufferAttribute(new Float32Array(values_index2), 1));
bufferLineGeo.addAttribute('norm', new THREE.BufferAttribute(values_bNorm, 3).copyVector3sArray(values_norm));*/
hair = new THREE.Line(//bufferLineGeo,
lineGeo,
shaderMaterial, THREE.LineStrip );
scene.add(hair);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function render() {
requestAnimationFrame(render);
time = new Date().getTime();
delta = time - oldTime;
oldTime = time;
if (isNaN(delta) || delta > 1000 || delta == 0 ) {
delta = 1000/60;
}
uniforms.globalTime.value += delta * 0.005;
renderer.render( scene, camera );
}
</script>
</body>
</html>
You can get a copy of three.min.js for version 58 here.
You can get the current version of three.min.js from here.
This line
bufferLineGeo.fromGeometry( lineGeo );
will not populate the position attribute if no faces are defined.
Do this instead
var positions = new Float32Array( vertices.length * 3 );
bufferLineGeo.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ).copyVector3sArray( vertices ) );
three.js r.89

WebGL - importing MD2 Model, problems with textures

I'm making a webgl demo with md2 models. I know that it is an old format but I need to use only md2.
I read the documentation for the md2 format.
I watched how this site works:
and used part of it's sources in my program (i saved the author's name in the comments)
I made vertex loading, the model loads great!
But when I'm trying to map the texutre, something strange happens:
First, I thought that it's the problem of fragment shader, but I made an export by using assimp2json, used the data from it and it was drawn as it should draw.
The problem is that assimp2json changed the order vertices in triangles and uvs order, so I can't debug the program using it.
Maybe, someone could help to find the bug, to point to the error in my code?
P. S. since there is no animation, I only use the first frame
The most interesting is that if I will pass unindexed data (just the uvs from the file) it looks more right than the indexed textures:
The problem is that it is wrong in some places, like here:
Full source code and models:
ShaderProgram.js
class ShaderProgram
{
constructor(gl, VSSource, FSSource) // VS - Vertex Shader, FS - Fragment
Shader
{
this.gl = gl;
let vertexShader = this.getShader(VSSource, gl.VERTEX_SHADER);
let fragmentShader = this.getShader(FSSource, gl.FRAGMENT_SHADER);
this.shaderProgram = gl.createProgram();
gl.attachShader(this.shaderProgram, vertexShader);
gl.attachShader(this.shaderProgram, fragmentShader);
gl.linkProgram(this.shaderProgram);
if (!gl.getProgramParameter(this.shaderProgram, gl.LINK_STATUS)) {
alert("Can't load shaders");
}
this.enableAttributes();
this.getUniforms();
}
getShader(source, type)
{
let gl = this.gl;
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert("Error compilation: " + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
getUniforms()
{
this.model = gl.getUniformLocation(this.shaderProgram, "model");
this.view = gl.getUniformLocation(this.shaderProgram, "view");
this.projection = gl.getUniformLocation(this.shaderProgram, "projection");
}
enableAttributes()
{
let gl = this.gl;
let shaderProgram = this.shaderProgram;
}
use()
{
this.gl.useProgram(this.shaderProgram);
}
}
Textures.js
function initTexture(filename) {
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
let image = new Image();
image.onload = function() { handleTextureLoaded(image, texture); }
image.src = filename;
return texture;
}
function handleTextureLoaded(image, texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
}
MD2 Import:
// BinaryReader
// Refactored by Vjeux <vjeuxx#gmail.com>
// http://blog.vjeux.com/2010/javascript/javascript-binary-reader.html
// Original
//+ Jonas Raoni Soares Silva
//# http://jsfromhell.com/classes/binary-parser [rev. #1]
BinaryReader = function (data) {
this._buffer = data;
this._pos = 0;
};
BinaryReader.prototype = {
/* Public */
readInt8: function (){ return this._decodeInt(8, true); },
readUInt8: function (){ return this._decodeInt(8, false); },
readInt16: function (){ return this._decodeInt(16, true); },
readUInt16: function (){ return this._decodeInt(16, false); },
readInt32: function (){ return this._decodeInt(32, true); },
readUInt32: function (){ return this._decodeInt(32, false); },
readFloat: function (){ return this._decodeFloat(23, 8); },
readDouble: function (){ return this._decodeFloat(52, 11); },
readChar: function () { return this.readString(1); },
readString: function (length) {
this._checkSize(length * 8);
var result = this._buffer.substr(this._pos, length);
this._pos += length;
return result;
},
seek: function (pos) {
this._pos = pos;
this._checkSize(0);
},
getPosition: function () {
return this._pos;
},
getSize: function () {
return this._buffer.length;
},
/* Private */
_decodeFloat: function(precisionBits, exponentBits)
{
return this._decodeFloat2(precisionBits, exponentBits);
var length = precisionBits + exponentBits + 1;
var size = length >> 3;
this._checkSize(length);
var bias = Math.pow(2, exponentBits - 1) - 1;
var signal = this._readBits(precisionBits + exponentBits, 1, size);
var exponent = this._readBits(precisionBits, exponentBits, size);
var significand = 0;
var divisor = 2;
var curByte = length + (-precisionBits >> 3) - 1;
do
{
var byteValue = this._readByte(++curByte, size);
var startBit = precisionBits % 8 || 8;
var mask = 1 << startBit;
while (mask >>= 1)
{
if (byteValue & mask)
{
significand += 1 / divisor;
}
divisor *= 2;
}
} while (precisionBits -= startBit);
this._pos += size;
return exponent == (bias << 1) + 1 ? significand ? NaN : signal ? -Infinity : +Infinity
: (1 + signal * -2) * (exponent || significand ? !exponent ? Math.pow(2, -bias + 1) * significand
: Math.pow(2, exponent - bias) * (1 + significand) : 0);
},
// I added this because _decodeFloat gave me some real inaccuarate results? -Terry Butler
_decodeFloat2: function(precisionBits, exponentBits)
{
var length = precisionBits + exponentBits + 1;
var value = this._decodeInt(length);
var sign = (value >> 31) & 0x1;
var allZero = 1;
var mantissa = 0.0;
var exponent = 0.0;
// Mantissa
for (var i = 22; i > -1; i--)
{
var test = 1.0 / Math.pow(2, 23-i);
if ((value >> i & 0x1) == 1)
{
mantissa += test;
allZero = 0;
}
}
if (allZero == 0)
mantissa += 1.0;
// Exponent
for (var i = 30; i > 22; i--)
{
var test = Math.pow(2, i - 23);
if ((value >> i & 0x1) == 1)
{
exponent += test;
}
}
exponent -= 127.0;
//
var total = Math.pow(2.0, exponent) * mantissa;
//
if (sign == 1)
{
total *= -1.0;
}
return total;
},
_decodeInt: function(bits, signed){
var x = this._readBits(0, bits, bits / 8), max = Math.pow(2, bits);
var result = signed && x >= max / 2 ? x - max : x;
this._pos += bits / 8;
return result;
},
//shl fix: Henri Torgemane ~1996 (compressed by Jonas Raoni)
_shl: function (a, b){
for (++b; --b; a = ((a %= 0x7fffffff + 1) & 0x40000000) == 0x40000000 ? a * 2 : (a - 0x40000000) * 2 + 0x7fffffff + 1);
return a;
},
_readByte: function (i, size) {
return this._buffer.charCodeAt(this._pos + size - i - 1) & 0xff;
},
_readBits: function (start, length, size) {
var offsetLeft = (start + length) % 8;
var offsetRight = start % 8;
var curByte = size - (start >> 3) - 1;
var lastByte = size + (-(start + length) >> 3);
var diff = curByte - lastByte;
var sum = (this._readByte(curByte, size) >> offsetRight) & ((1 << (diff ? 8 - offsetRight : length)) - 1);
if (diff && offsetLeft) {
sum += (this._readByte(lastByte++, size) & ((1 << offsetLeft) - 1)) << (diff-- << 3) - offsetRight;
}
while (diff) {
sum += this._shl(this._readByte(lastByte++, size), (diff-- << 3) - offsetRight);
}
return sum;
},
_checkSize: function (neededBits) {
if (!(this._pos + Math.ceil(neededBits / 8) < this._buffer.length)) {
throw new Error("Index out of bound");
}
}
};
/**
* #author oosmoxiecode
* based on http://www.terrybutler.co.uk/web-development/html5-canvas-md2- renderer/
* and
* http://tfc.duke.free.fr/coding/md2-specs-en.html
*
* dependant on binaryReader.js
*
* Returns a object like: {string: json_string, info: {status: "Success", faces: 10, vertices: 10, frames: 5 }}
*
**/
// Library is modified for this program by me
function MD2_converter (file) {
var scope = this;
// Create the Binary Reader
var reader = new BinaryReader(file);
// Setup
var header = {};
var frames = [];
var st = [];
var triag = [];
var string = "";
var info = {};
var returnObject = {string: string, info: info};
// Ident and version
header.ident = reader.readString(4);
header.version = reader.readInt32();
// Valid MD2 file?
if (header.ident != "IDP2" || header.version != 8) {
info.status = "Not a valid MD2 file";
return returnObject;
}
// header
header.skinwidth = reader.readInt32(); // texture width
header.skinheight = reader.readInt32(); // texture height
header.framesize = reader.readInt32(); // size in bytes of a frame
header.num_skins = reader.readInt32(); // number of skins
header.num_vertices = reader.readInt32(); // number of vertices per frame
header.num_st = reader.readInt32(); // number of texture coordinates
header.num_tris = reader.readInt32(); // number of triangles
header.num_glcmds = reader.readInt32(); // number of opengl commands
header.num_frames = reader.readInt32(); // number of frames
header.offset_skins = reader.readInt32(); // offset skin data
header.offset_st = reader.readInt32(); // offset texture coordinate data
header.offset_tris = reader.readInt32(); // offset triangle data
header.offset_frames = reader.readInt32(); // offset frame data
header.offset_glcmds = reader.readInt32(); // offset OpenGL command data
header.offset_end = reader.readInt32(); // offset end of file
// faulty size
if (reader.getSize() != header.offset_end) {
info.status = "Corrupted MD2 file";
return returnObject;
}
// texture coordinates
let count = 0;
reader.seek(header.offset_st);
for (var i = 0; i < header.num_st; i++) {
var s = reader.readInt16();
var t = reader.readInt16();
st[i] = {
s: s / header.skinwidth,
t: t / header.skinheight
};
}
reader.seek(header.offset_tris);
for (var i = 0; i < header.num_tris; i++) {
var a = reader.readInt16();
var b = reader.readInt16();
var c = reader.readInt16();
var uva_i = reader.readUInt16();
var uvb_i = reader.readUInt16();
var uvc_i = reader.readUInt16();
triag[i] = {};
triag[i].vertex = [];
triag[i].st = [];
triag[i].vertex[0] = a;
triag[i].vertex[1] = b;
triag[i].vertex[2] = c;
triag[i].st[0] = uva_i;
triag[i].st[1] = uvb_i;
triag[i].st[2] = uvc_i;
}
// frames
reader.seek(header.offset_frames);
for (var f = 0; f < header.num_frames; f++) {
var frame = {};
frame.name = "";
frame.scale = {};
frame.translate = {};
frame.scale.x = reader.readFloat();
frame.scale.y = reader.readFloat();
frame.scale.z = reader.readFloat();
frame.translate.x = reader.readFloat();
frame.translate.y = reader.readFloat();
frame.translate.z = reader.readFloat();
frame.vertices = [];
frame.name = reader.readString(16).replace(/[^a-z0-9]/gi,''); // 4+4+4 4+4+4 (12 + 12) = 24 + 16 = 40
for (var v = 0; v < header.num_vertices; v++) {
var tempX = reader.readUInt8();
var tempY = reader.readUInt8();
var tempZ = reader.readUInt8();
var normal = reader.readUInt8();
var xx = frame.scale.x * tempX + frame.translate.x;
var yy = frame.scale.z * tempZ + frame.translate.z;
var zz = frame.scale.y * tempY + frame.translate.y;
let vertex = [];
vertex[0] = xx;
vertex[1] = yy;
vertex[2] = zz;
frame.vertices.push(vertex);
}
frames.push(frame);
}
let res = {};
res.st = st;
res.triag = triag;
res.frame = [];
for (var i=0; i<frames.length; ++i )
res.frame[i]=frames[i];
res.faces_count = header.num_tris;
res.vertices_count = header.num_vertices;
res.frames_count = header.num_frames;
return res;
}
main.js
let gl;
let shaderProgram;
let firstMouse = true;
let mouseDown = false;
let lastTime = 0;
let deltaTime;
let test;
let lastX= 0;
let lastY = 0;
let DIRS = {
Forward: 0,
Backward: 1,
Left: 2,
Right: 3
};
let KeyCodes =
{
Up: 38,
Down : 40,
Left : 37,
Right : 39,
W : 87,
S : 83,
A : 65,
D : 68,
Q : 81,
E : 69,
PageDown : 34,
PageUp : 33
};
let pressedKeys = [];
let vertexShaderSource = `
precision highp float;
attribute vec3 aPos;
attribute vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
varying vec2 vTexCoord;
void main(void)
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vTexCoord = aTexCoord;
}
`;
let fragmentShaderSource = `
precision highp float;
varying highp vec2 vTexCoord;
uniform sampler2D uSampler;
void main(void)
{
gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s,1.0-vTexCoord.t));
}
`;
let camera;
function initGL()
{
let canvas = document.getElementById("Canvas3D");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
try
{
gl = canvas.getContext("webgl2") || canvas.getContext("experimental-webgl2");
}
catch(e)
{
alert("Your browser don't support WebGL");
}
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);
shaderProgram = new ShaderProgram(gl,vertexShaderSource,fragmentShaderSource);
shaderProgram.use();
camera = {
Init(shaderProgram)
{
this.shaderProgram = shaderProgram;
this.pos = vec3.create();
this.up = vec3.create();
this.front = vec3.create();
this.right = vec3.create();
this.WorldUp = vec3.create();
this.pos = [51.656294013839215, 36.9293564686086, -28.23351748054847];
this.front = [-0.7667674915573891, -0.6401096994849556, 0.04824092159224934];
this.up = [-0.6388465891741784, 0.7682835235935235, 0.040192821190338686];
this.WorldUp = [0,1,0];
this.yaw = 176.39999999999998;
this.pitch = -39.8;
this.speed = 25;
this.sensivity = 0.1;
this.zoom = 100;
this.view = mat4.create();
this.projection = mat4.create();
mat4.perspective(this.projection, Math.PI/180.0*this.zoom, gl.viewportWidth/gl.viewportHeight, 0.1, 200);
this.shaderProgram.gl.uniformMatrix4fv(this.shaderProgram.projection, false, this.projection);
this.updateCameraVectors();
this.updateView();
},
updateView: function()
{
let tmp = vec3.create();
vec3.add(tmp, this.pos, this.front);
mat4.lookAt(this.view, this.pos, tmp, this.up);
this.shaderProgram.gl.uniformMatrix4fv(this.shaderProgram.view, false, this.view);
},
processKeyboard(deltaTime)
{
let velocity = this.speed * deltaTime;
let tmp = vec3.create();
if (pressedKeys[KeyCodes.W] || pressedKeys[KeyCodes.Up]) {
vec3.scale(tmp, this.front, velocity);
vec3.add(this.pos,this.pos,tmp);
}
if (pressedKeys[KeyCodes.S] || pressedKeys[KeyCodes.Down]) {
velocity *=(-1);
vec3.scale(tmp, this.front, velocity);
vec3.add(this.pos,this.pos,tmp);
}
if (pressedKeys[KeyCodes.A] || pressedKeys[KeyCodes.Left]) {
velocity *=(-1);
vec3.scale(tmp, this.right, velocity);
vec3.add(this.pos,this.pos,tmp);
}
if (pressedKeys[KeyCodes.D] || pressedKeys[KeyCodes.Right]) {
vec3.scale(tmp, this.right, velocity);
vec3.add(this.pos,this.pos,tmp);
}
},
processMouseMovement(xoffset, yoffset)
{
this.yaw += (xoffset*this.sensivity);
this.pitch += (yoffset*this.sensivity);
if (this.pitch > 89.0)
this.pitch = 89.0;
if (this.pitch < -89.0)
this.pitch = -89.0;
this.updateCameraVectors();
},
updateCameraVectors()
{
let ToRads = Math.PI/180;
let yaw = this.yaw;
let pitch = this.pitch;
this.front[0] = Math.cos(ToRads*yaw) * Math.cos(ToRads*pitch);
this.front[1] = Math.sin(ToRads*pitch);
this.front[2] = Math.sin(ToRads*yaw) * Math.cos(ToRads*pitch);
vec3.normalize(this.front, this.front);
vec3.cross(this.right, this.front, this.WorldUp);
vec3.normalize(this.right, this.right);
vec3.cross(this.up, this.right, this.front);
vec3.normalize(this.up, this.up);
}
};
function load_binary_resource(url) {
let req = new XMLHttpRequest();
req.open('GET', url, false);
req.overrideMimeType('text/plain; charset=x-user-defined'); // No unicode data
req.send(null);
if (req.status != 200) return '';
return req.responseText;
}
let file = load_binary_resource('../models/rhino/Tris.md2');
let res = MD2_converter(file);
let texture = initTexture("../models/rhino/rhino.png");
let verts = [];
let inds = [];
let uvs = [];
for (let i =0;i<res.frame[0].vertices.length;i++)
{
let vert = res.frame[0].vertices[i];
verts.push(vert[0]);
verts.push(vert[1]);
verts.push(vert[2]);
}
for (let i =0;i<res.triag.length;i++)
{
let triag = res.triag[i];
inds.push(triag.vertex[0]);
inds.push(triag.vertex[1]);
inds.push(triag.vertex[2]);
uvs.push(res.st[triag.st[0]].s);
uvs.push(res.st[triag.st[0]].t);
uvs.push(res.st[triag.st[1]].s);
uvs.push(res.st[triag.st[1]].t);
uvs.push(res.st[triag.st[2]].s);
uvs.push(res.st[triag.st[2]].t);
}
test = new Drawable(shaderProgram, verts, inds, uvs, texture);
camera.Init(shaderProgram);
requestAnimationFrame(gameCycle);
}
function recalculateFPS(gotTime)
{
deltaTime = (gotTime - lastTime)/1000;
lastTime = gotTime;
}
function gameCycle(gotTime)
{
recalculateFPS(gotTime);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
camera.processKeyboard(deltaTime);
camera.updateView();
test.draw();
requestAnimationFrame(gameCycle);
}
window.onload=function()
{
initGL();
};
document.onkeydown = function(e)
{
pressedKeys[e.keyCode]=true;
};
document.onkeyup = function(e)
{
pressedKeys[e.keyCode]=false;
};
document.body.onmousedown = function(event)
{
mouseDown = true;
lastX = event.clientX;
lastY = event.clientY;
};
document.body.onmouseup = function(event)
{
mouseDown = false;
};
document.body.onmouseout = function(event)
{
mouseDown = false;
};
document.body.onmousemove = function (e)
{
if (!mouseDown)
return;
let xpos = e.clientX;
let ypos = e.clientY;
if(firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
let xoffset = xpos - lastX;
let yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera.processMouseMovement(xoffset,yoffset);
}
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body scroll="no" style="overflow: hidden">
<canvas id="Canvas3D"></canvas>
<script src="../js/glmatrix/dist/gl-matrix.js"></script>
<script src="../js/MD2Import.js"></script>
<script src="../js/ShaderProgram.js"></script>
<script src="../js/Textures.js"></script>
<script src="../js/Drawable.js"></script>
<script src="../js/main.js"></script>
</body>
</html>
Drawable.js
class Drawable {
constructor(shaderProgram, vertices, indices, texCoords, texture) {
this.gl = shaderProgram.gl;
let gl = shaderProgram.gl;
this.shaderProgram = shaderProgram;
shaderProgram.aTexCoord = gl.getAttribLocation(shaderProgram.shaderProgram, "aTexCoord");
shaderProgram.aPos = gl.getAttribLocation(shaderProgram.shaderProgram, "aPos");
this.VAO = gl.createVertexArray();
this.texture = texture;
gl.bindVertexArray(this.VAO);
this.vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.aPos, 3, gl.FLOAT, false, 12, 0);
gl.enableVertexAttribArray(shaderProgram.aPos);
this.EBO = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.EBO);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
this.VBOTexCoords = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.VBOTexCoords);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords),gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.aTexCoord, 2, gl.FLOAT, false, 8, 0);
gl.enableVertexAttribArray(shaderProgram.aTexCoord);
gl.bindVertexArray(null);
this.pos = vec3.create();
this.rot = vec3.create(); // в Градусах
this.scale = vec3.create();
this.pos = [0,0,0];
this.rot = [0,0,0];
this.scale=[1,1,1];
this.vertCount = indices.length;
this.model = mat4.create();
}
updateModel() {
mat4.identity(this.model);
mat4.translate(this.model, this.model, this.pos);
mat4.rotateX(this.model, this.model, Math.PI / 180 * this.rot[0]);
mat4.rotateY(this.model, this.model, Math.PI / 180 * this.rot[1]);
mat4.rotateZ(this.model, this.model, Math.PI / 180 * this.rot[2]);
mat4.scale(this.model, this.model, this.scale);
}
translate(transX, transY, transZ)
{
this.pos[0] += transX;
this.pos[1] += transY;
this.pos[2] += transZ;
}
rotate(rotX, rotY, rotZ)
{
this.rot[0] += rotX;
this.rot[1] += rotY;
this.rot[2] += rotZ;
}
draw() {
this.updateModel();
let gl =this.gl;
gl.uniformMatrix4fv(this.shaderProgram.model, false, this.model);
gl.bindVertexArray(this.VAO);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.uniform1i(gl.getUniformLocation(shaderProgram.shaderProgram, "uSampler"), 0);
//gl.drawArrays(gl.TRIANGLES, 0, this.vertCount);
gl.drawElements(gl.TRIANGLES, this.vertCount,gl.UNSIGNED_SHORT,0);
}
}
Tried using gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true), result:
By the way, it shouldn't work, because I already flipped the textures in the shader:
gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s,1.0 - vTexCoord.t));
rhino.png (textures)
The md2 file: http://dropmefiles.com/zhQFU
The problem solved when I deleted the index buffer and placed the vertices and uvs in one huge buffer. What it could be?
The solution was to delete index buffer and place everything in the vertex buffer. It worked for me

Low FPS in WebGL (working as 2d)

I have script
https://drive.google.com/file/d/0B4Cqle1HMxo8LXl6YktuMF9EVWc/view?usp=sharing
It's working as WebGL 2D
When it draws 50 images (texture) -- FPS 60, but when it draws 150 and more -- FPS 20-30
Why?
How can I solve this problem with WebGL?
UPD
jsFiddle
html
<canvas id="spirit_canvas"></canvas>
<div id="fps" style="position:absolute;top:0;left:0;background:rgba(0,0,0,0.1);color:#111;padding:1px 2px;font-size:10px;font-family:sans-serif;z-index:5"></div>
js
function WebGL2d(id)
{
this._el = document.getElementById(id);
this._gl = null;
this._vertexShader = null;
this._fragmentShader = null;
this._program = null;
this._p = {
positionLocation: null,
translationLocation: null,
resolutionLocation: null,
colorLocation: null,
texCoordLocation: null,
v_t: null
};
this._vertexShaderSrc = "\
attribute vec2 a_position;\n\
uniform vec2 u_resolution;\n\
uniform vec2 u_translation;\n\
attribute vec2 a_texCoord;\n\
varying vec2 v_texCoord;\n\
void main() {\n\
vec2 position = a_position + u_translation;\n\
vec2 zeroToOne = position / u_resolution;\n\
vec2 zeroToTwo = zeroToOne * 2.0;\n\
vec2 clipSpace = zeroToTwo - 1.0;\n\
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n\
gl_PointSize = 2.;\n\
v_texCoord = a_texCoord;\n\
}\n\
";
this._fragmentShaderSrc = "\
precision mediump float;\n\
uniform vec4 u_color;\n\
uniform sampler2D u_image;\n\
varying vec2 v_texCoord;\n\
uniform int v_t;\n\
void main() {\n\
gl_FragColor = u_color;\n\
if (v_t == 1) {\n\
gl_FragColor = texture2D(u_image, v_texCoord);\n\
}\n\
}\n\
";
this._canvas2d = null;
this._canvas2dCache = null;
this._canvasPathBuffer = [];
this._isPointInPath = false;
this.txtr = {};
this._vertexBuffer = null;
this._indexBuffer = null;
this._uvBuffer = null;
this._colorBuffer = null;
// -------------------
this.fillStyle = '#000';
this.strokeStyle = '#000';
this.lineWidth = 1;
//----|||
this._init();
}
WebGL2d.prototype = {
_getCanvas: function(w,h)
{
var canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
return canvas.getContext("2d");
},
_hexToRgbArray: function (hex) {
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
1
] : [0,0,0,1];
},
_rgbToArray: function(rgba) {
var result = /(?:rgb|rgba)\((\d+),\s?(\d+),\s?(\d+)(?:,\s?(\d+|\d.\d+))?\)/g.exec(rgba);
if(result) {
return result.slice(1).filter(isFinite).map(parseFloat);
} else {
return [0,0,0,1];
}
},
_context: function () {
var names = ["webgl","experimental-webgl"];
var context = null;
for (var ii = 0; ii < names.length; ++ii) {
try {
context = this._el.getContext(names[ii], {/*alpha: true, premultipliedAlpha: true, antialiasing: true*/});
} catch (e) {
}
if (context) {
break;
}
}
if (context === null) {
console.error('WebGL2d don\'t init');
return;
}
this._gl = context;
console.log('--');
},
_loadShader: function (src, type) {
var shader = this._gl.createShader(type);
this._gl.shaderSource(shader, src);
this._gl.compileShader(shader);
var compiled = this._gl.getShaderParameter(shader, this._gl.COMPILE_STATUS);
if (!compiled) {
lastError = this._gl.getShaderInfoLog(shader);
console.error("*** Error compiling shader '" + shader + "':" + lastError);
this._gl.deleteShader(shader);
return null;
}
return shader;
},
_loadProgram: function (shaders) {
var program = this._gl.createProgram();
for (var i = 0; i < shaders.length; ++i) {
this._gl.attachShader(program, shaders[i]);
}
this._gl.linkProgram(program);
var linked = this._gl.getProgramParameter(program, this._gl.LINK_STATUS);
if (!linked) {
lastError = this._gl.getProgramInfoLog(program);
log.error("Error in program linking:" + lastError);
this._gl.deleteProgram(program);
return null;
}
return program;
},
_init: function () {
this._context();
this._canvas2d = this._getCanvas(this._el.width,this._el.height);
//this._gl.disable(this._gl.DEPTH_TEST);
this._gl.enable(this._gl.BLEND);
this._gl.blendFunc(this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA);
//this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
//this._gl.clearDepth (0.0);
//this._gl.clearColor(0, 0, 0, 1.0);
//this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT);
//this._gl.clear(this._gl.COLOR_BUFFER_BITT);
this._vertexShader = this._loadShader(this._vertexShaderSrc, this._gl.VERTEX_SHADER);
this._fragmentShader = this._loadShader(this._fragmentShaderSrc, this._gl.FRAGMENT_SHADER);
this._program = this._loadProgram([this._vertexShader, this._fragmentShader]);
this._gl.useProgram(this._program);
this._p.positionLocation = this._gl.getAttribLocation(this._program, "a_position");
this._p.translationLocation = this._gl.getAttribLocation(this._program, "u_translation");
this._p.resolutionLocation = this._gl.getUniformLocation(this._program, "u_resolution");
this._p.colorLocation = this._gl.getUniformLocation(this._program, "u_color");
// texture
this._p.texCoordLocation = this._gl.getAttribLocation(this._program, "a_texCoord");
this._p.v_t = this._gl.getUniformLocation(this._program, "v_t");
this._gl.uniform2f(this._p.resolutionLocation, this._el.width, this._el.height);
this._initBuff();
},
_initBuff: function()
{
this._vertexBuffer = this._gl.createBuffer();
this._indexBuffer = this._gl.createBuffer();
this._uvBuffer = this._gl.createBuffer();
this._colorBuffer = this._gl.createBuffer();
},
_drawArrTriangle: function(num)
{
this._gl.drawArrays(this._gl.TRIANGLE_STRIP, 0,num);
},
// ==========
_setColor: function (color)
{
this._gl.uniform1i(this._p.v_t, 0);
if (!(color instanceof Array)) {
if (color.indexOf('r') === 0 || color.indexOf('R') === 0) {
color = this._rgbToArray(color);
} else {
color = this._hexToRgbArray(color);
}
}
color[0] = Math.round(color[0] / 255 * 100)/100;
color[1] = Math.round(color[1] / 255 * 100)/100;
color[2] = Math.round(color[2] / 255 * 100)/100;
this._gl.uniform4f(this._p.colorLocation, color[0], color[1], color[2], color[3]);
},
_buff: function (arr)
{
this._gl.enableVertexAttribArray(this._p.positionLocation);
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._vertexBuffer);
this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array(arr), this._gl.DYNAMIC_DRAW);
this._gl.vertexAttribPointer(this._p.positionLocation, 2, this._gl.FLOAT, false, 0, 0);
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, null);
},
_buffTexture: function(image)
{
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._uvBuffer);
this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0]), this._gl.STATIC_DRAW);
this._gl.enableVertexAttribArray(this._p.texCoordLocation);
this._gl.vertexAttribPointer(this._p.texCoordLocation, 2, this._gl.FLOAT, false, 0, 0);
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, null);
if (!(image.src in this.txtr)) {
this.txtr[image.src] = this._gl.createTexture();
this._gl.bindTexture(this._gl.TEXTURE_2D, this.txtr[image.src]);
// Set the parameters so we can render any size image.
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE);
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE);
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.LINEAR);
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.LINEAR);
this._gl.texImage2D(this._gl.TEXTURE_2D, 0, this._gl.RGBA, this._gl.RGBA, this._gl.UNSIGNED_BYTE, image);
} else {
this._gl.bindTexture(this._gl.TEXTURE_2D, this.txtr[image.src]);
}
},
//--------------------------------------------
fillRect: function(x,y,w,h,color,image)
{
var translation = [0, 0];
translation[0] = x;
translation[1] = y;
var x2 = x + w;
var y2 = h + y;
this._buff(
[
x,y,
x2,y,
x,y2,
x,y2,
x2,y,
x2,y2
]
);
if (!image) {
if (color) {
this._setColor(color);
} else {
this._setColor(this.fillStyle);
}
}
this._drawArrTriangle(6);
},
strokeRect: function(x, y, w, h)
{
this.fillRect(
x,
y,
w,
this.lineWidth,
this.strokeStyle
);
this.fillRect(
(x + w - this.lineWidth),
y,
this.lineWidth,
h,
this.strokeStyle
);
this.fillRect(
x,
(y + h - this.lineWidth),
w,
this.lineWidth,
this.strokeStyle
);
this.fillRect(
x,
y,
this.lineWidth,
h,
this.strokeStyle
);
},
clearRect: function(x, y, w, h)
{
//this._gl.clearDepth (1.0);
this._gl.clearColor(0, 0, 0, 1.0);
//this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT);
//this._gl.clear(this._gl.COLOR_BUFFER_BIT);
},
beginPath: function(isPointInPath)
{
if (isPointInPath == true) {
this._isPointInPath = true;
this._canvas2d.beginPath();
} else {
this._isPointInPath = false;
this._canvasPathBuffer = [];
}
},
moveTo: function(x,y)
{
if (this._isPointInPath == true) {
this._canvas2d.moveTo(x,y);
} else {
this._canvasPathBuffer.push([x,y]);
}
},
lineTo: function(x,y)
{
if (this._isPointInPath == true) {
this._canvas2d.lineTo(x,y);
} else {
this._canvasPathBuffer.push([x,y]);
}
},
fill: function()
{
if (this._canvasPathBuffer.length == 4) {
this._buff(
[
this._canvasPathBuffer[0][0], this._canvasPathBuffer[0][2],
this._canvasPathBuffer[1][0], this._canvasPathBuffer[1][3],
this._canvasPathBuffer[2][0], this._canvasPathBuffer[2][4],
this._canvasPathBuffer[2][0], this._canvasPathBuffer[2][5],
this._canvasPathBuffer[3][0], this._canvasPathBuffer[3][6],
this._canvasPathBuffer[0][0], this._canvasPathBuffer[0][7]
]
);
this._setColor(this.fillStyle);
this._drawArrTriangle(6);
}
},
closePath: function()
{
if (this._isPointInPath == true) {
this._canvas2d.closePath();
}
},
isPointInPath: function(x,y)
{
return this._canvas2d.isPointInPath(x,y);
},
text: function (text,x,y,size,color,fontStyle,fontFamily,borderColor)
{
return false;
},
fillText: function(text,x,y,maxWidth)
{
return false;
},
drawImage: function(img,x,y, w, h)
{
this._gl.uniform1i(this._p.v_t, 1);
this._buffTexture(img);
this.fillRect(x,y,w,h,false,true);
}
};
function microtime()
{
return new Date().getTime();
}
function round(s,exp) {
exp = exp || 0;
return Math.round(s * Math.pow(10,exp)) / Math.pow(10,exp);
}
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element){
window.setTimeout(callback, 1000 / 60);
};
})();
var canvas;
var image;
var fpsLastCalledTime;
var height = 400;
var width = 700;
function main() {
var $el = $('#spirit_canvas');
$('body').css('width',(width + 'px'));
$el.css({'width':(width + 'px'),'height':(height + 'px')}).attr('width',width).attr('height',height);
canvas = new WebGL2d('spirit_canvas');
image = new Image();
image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAlCAYAAABcZvm2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWNJREFUeNrsV8sNwjAMbUqBBWACxB2pQ8AKcGALTsAJuDEFB1gBhuDAuWICmICPQh01pXWdJqEFcaglRGRbfonjPLuMc+5QwhjLGEJfZusjxZOL9akZKye9G98vPMfvsAx4qBfKwfzBL9s6uUHpI6U/u7+BKGkNb/H6umtk7MczF0HyfKS4zo/k/4AgTV8DOizrqX8oECgC+MGa8lGJp9sJDiAB8nyqYoglvJOPbP97IqoATGxWVZeXJlMQwYHA3piF8wJIblOVNBBxe3TPMLoHIKtxrbS7AAbBrA4Y5NaPAXf8LjN6wKZ0RaZOnlAFZnuXInVR4FTE6eYp0olPhhshtXsAwY3PquoAJNkIY33U7HTs7hYBwV24ItUKqDwgKF3VzAZ6k8HF+B1BMF8xRJbeJoqMXHZAAQ1kwoluURCdzepEugGEImBrIADB7I4lyfbJLlw92FKE6b5hVd+ktv4vAQYASMWxvlAAvcsAAAAASUVORK5CYII=";
image.onload = function() {
play();
}
}
function play()
{
//this.time = microtime();
canvas.clearRect();
draw();
drawFps();
requestAnimFrame(play.bind(window));
}
drawFps = function()
{
var fps;
if(!fpsLastCalledTime) {
fpsLastCalledTime = microtime();
fps = 0;
} else {
fps = round(1000/(microtime() - fpsLastCalledTime));
fpsLastCalledTime = microtime();
}
$('#fps').html('fps: ' + fps);
};
function draw()
{
canvas.fillRect(0,0,width,height,'#eee');
var __x = 0;
var __y = 0;
var __h = 37;
var __w = 26;
var x, y, h, w;
for(var i = 0; i < 200; ++i) {
if (i % 26 == 0) {
__y = __y + __h;
__x = 0;
} else {
__x = __x + __w;
//__y = __y;
}
h = __h;
w = __w;
x = __x;
y = __y;
//canvas.fillRect(x,y, w, h,'#050');
canvas.drawImage(image,x,y, w, h);
}
}
main();
In general you only want to call gl.bufferData and gl.texImage2d at init time. AFAICT you're calling them for every draw call.

How to draw 3d objects on a 2d canvas

here's the full code, I had to remove spaces from some of the functions that weren't related to the problem to make sure im in the 30k character limit of stack overflow
const EPSILON = 0.000001;
const mat4 = { rotateZ: function(out, a, rad) { let s = Math.sin(rad); let c = Math.cos(rad); let a00 = a[0]; let a01 = a[1]; let a02 = a[2]; let a03 = a[3]; let a10 = a[4]; let a11 = a[5]; let a12 = a[6]; let a13 = a[7]; if (a !== out) { out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } out[0] = a00 * c + a10 * s; out[1] = a01 * c + a11 * s; out[2] = a02 * c + a12 * s; out[3] = a03 * c + a13 * s; out[4] = a10 * c - a00 * s; out[5] = a11 * c - a01 * s; out[6] = a12 * c - a02 * s; out[7] = a13 * c - a03 * s; return out; }, create: function() { let out = new Float32Array(16); out[0] = 1; out[5] = 1; out[10] = 1; out[15] = 1; return out; }, perspective: function(out, fovy, aspect, near, far) { let f = 1.0 / Math.tan(fovy / 2), nf; out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[11] = -1; out[12] = 0; out[13] = 0; out[15] = 0; if (far !== null && far !== Infinity) { nf = 1 / (near - far); out[10] = (far + near) * nf; out[14] = (2 * far * near) * nf; } else { out[10] = -1; out[14] = -2 * near; } return out; }, translate: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; if (a === out) { out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; return out; } else { let a00, a01, a02, a03; let a10, a11, a12, a13; let a20, a21, a22, a23; a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; out[12] = a00 * x + a10 * y + a20 * z + a[12]; out[13] = a01 * x + a11 * y + a21 * z + a[13]; out[14] = a02 * x + a12 * y + a22 * z + a[14]; out[15] = a03 * x + a13 * y + a23 * z + a[15]; return out; } }, scale: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; out[0] = a[0] * x; out[1] = a[1] * x; out[2] = a[2] * x; out[3] = a[3] * x; out[4] = a[4] * y; out[5] = a[5] * y; out[6] = a[6] * y; out[7] = a[7] * y; out[8] = a[8] * z; out[9] = a[9] * z; out[10] = a[10] * z; out[11] = a[11] * z; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }, multiply: function(out, a, b) { let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; return out; }, lookAt: function(out, eye, center, up) { let x0, x1, x2, y0, y1, y2, z0, z1, z2, len; let eyex = eye[0]; let eyey = eye[1]; let eyez = eye[2]; let upx = up[0]; let upy = up[1]; let upz = up[2]; let centerx = center[0]; let centery = center[1]; let centerz = center[2]; if (Math.abs(eyex - centerx) < EPSILON && Math.abs(eyey - centery) < EPSILON && Math.abs(eyez - centerz) < EPSILON) { return identity(out); } z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; len = 1 / Math.hypot(z0, z1, z2); z0 *= len; z1 *= len; z2 *= len; x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.hypot(x0, x1, x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.hypot(y0, y1, y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); out[15] = 1; return out; }, moveToVec3: function(out, v) { out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; } }; const mat3 = { clone: function(a) { let out = new Float32Array(9); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }, create: function() { let out = new Float32Array(9); out[0] = 1; out[4] = 1; out[8] = 1; return out; } }; const vec3 = { multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, create: function() { return new Float32Array(3);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; } }; const vec2 = { create: function() { return new Float32Array(2);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; return out; }, fromValues: function(x, y) { let out = new Float32Array(2); out[0] = x; out[1] = y; return out; }, multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, add: function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; return out; } };
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity{
constructor(){
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.multiTextures = false;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount){
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
}
class Display{
constructor(gl, programInfo, zAxis, texture){
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width/gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]).lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
this.last = {};
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {};
}
clear(color){
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.1, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
rect(x, y, w, h){
const {rect, stroke} = this.createRectPos(w, h);
const square = new Rect();
square.setup(...this.getRectInfo(x, y, rect, stroke));
return square;
}
fillRect(rect, color){
const {createStaticDrawBuffer, gl, parseColor} = this;
rect.fillers.fill = true;
if(color){
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
createRectPos(w, h){
const rect = [ w/2, h/2, -w/2, h/2, w/2, -h/2, -w/2, -h/2 ];
const stroke = [ -w/2, h/2, w/2, h/2, w/2, -h/2, -w/2, -h/2, ];
return {rect, stroke};
}
getRectInfo(x, y, rect, stroke){
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data){
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {gl, createStaticDrawBuffer} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length/2]; }
createAttribInfo(numComponents, type, normalize, stride, offset){
return { numComponents, type, normalize, stride, offset};
}
enableAttrib(buffer, attrib, gl, {numComponents, type, normalize, stride, offset}){
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents,type,normalize,stride,offset);
gl.enableVertexAttribArray(attrib);
}
drawBuffer(buffer){
const {gl, drawTexture, enableAttrib, createStaticDrawBuffer, currentCamera, texture: {context, canvas, textAttribInfo}, programInfo: {uniformLocations, program, attribLocations: {vertexPosition, vertexColor, textureCoord}}} = this;
const cameraMatrix = currentCamera.matrix;
const {positionsBuffer, fragColorPos, strokeColorPos, strokePositionBuffer, matrix, vertexAttribInfo, vertextColorAttribInfo, vertexCount, fragTextPos, fillers: {fill, stroke, texture}, strokeSize, textureInfo, multiTextures} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if(fill){
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6){
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Engine { constructor(time_step, update, render, allowedSkippedFrames) { this.accumulated_time = 0; this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false; this.update = update; this.render = render; this.allowedSkippedFrames = allowedSkippedFrames; this.run = this.run.bind(this); this.end = false; } run(time_stamp) { const { accumulated_time, time, time_step, updated, update, render, allowedSkippedFrames, end } = this; this.accumulated_time += time_stamp - time; this.time = time_stamp; if (accumulated_time > time_stamp * allowedSkippedFrames) { this.accumulated_time = time_stamp; } while (this.accumulated_time >= time_step) { this.accumulated_time -= time_step; update(time_stamp); this.updated = true; } if (updated) { this.updated = false; render(time_stamp); } if (end) { return; } this.animation_frame_request = requestAnimationFrame(this.run); } start() { this.accumulated_time = this.time_step; this.time = performance.now(); this.animation_frame_request = requestAnimationFrame(this.run); } stop() { this.end = true; cancelAnimationFrame(this.animation_frame_request); } }
class Entity extends Rect {
constructor(){
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args){
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args){
this.updateFillers.fill = args;
}
update(deltaTime, speed){
return this;
}
move(x, y){
super.move([x, y, this.coords[2]]);
return this;
}
}
class Quixotic{
constructor(display){
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0; this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args){
const display = this.display; const {rect, stroke} = display.createRectPos(5, 5); Class = Class ? Class : Entity; const className = Class.name; if(className !== "Entity" && !Entity.prototype.isPrototypeOf(Class.prototype)){ throw new TypeError("Expected extended class of Entity. Instead got: " + className); } let instance; const {objectsArray, classesInfo, objects} = this.world; const classInfo = classesInfo[className]; if(classInfo){ if(classInfo.args){ instance = new Class(...[...classInfo.args, ...args]); } else { instance = new Class(...args); } const name = classInfo.name; if(Array.isArray(objects[name])){ objects[name].push(instance); instance.name = name; } else { console.warn("Didn't save object in world.objects object, object wouldn't detect collision"); } } else { instance = new Class(...args); } instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000")); objectsArray.push(instance); return instance; }
createBackground(objects){
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {zAxis, canvas: {width, height}} = this.display;
zAxis--;
const halfZ = zAxis/2;
let {coords: [x, y], area: [w, h]} = objects[objects.length - 1];
let [mX, mY, mW, mH] = [x, y, w, h];
for(let i = objects.length-1; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
x < _x ? _x : x;
y < _y ? _y : y;
if(mX < _x){
mX = _x;
mW = _w;
}
if(mY < _y){
mY = _y;
mH = _h;
}
}
buffer.canvas.width = width;
buffer.canvas.height = height;
for(let i = objects.length; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
buffer.fillRect(((_x-halfZ-_w*2)/zAxis+1)*width, ((-_y-halfZ-_h*2)/zAxis+1)*height, _w*2/zAxis*width, _h*2/zAxis*height);
}
document.body.appendChild(buffer.canvas)
}
buildWorld({objects, classes, tileMap}){
const world = this.world;
if(Array.isArray(objects)){
for(let i = objects.length - 1; i > -1; i --){
const object = objects[i];
const {name, array, amount, position, collision, args, area} = object;
let createClass;
if(!object.class){
createClass = Entity;
}
const _args = args ? args : [];
let pos;
if(position){
let p = amount;
if(array){
const positions = position.positions;
pos = function(){
p--;
return positions[p];
};
} else {
pos = function(){
return position.position;
};
}
}
if(array){
let _array = [];
for(let j = amount; j--;){
const instance = this.createEntity(createClass, ..._args);
instance.name = name;
if(position){
instance.move(...pos());
}
if(area){
instance.setSize(area);
}
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
}
return;
}
setup(game){
const {style: {backgroundColor, backgroundImage, stroke}, world, engine: {frameRate, update, render}, setup} = game; this.buildWorld(world); const {display, entitySystem, world: {objectsArray, objects}} = this; if(backgroundImage){ display.gl.canvas.style.background = `url(${backgroundImage})`; if(repeatX || repeatY){ console.log("not read yet"); } } this.frameRate = frameRate; let lastUpdated = 0; this.update = (time) =>{ let deltaTime = time - lastUpdated; lastUpdated = time; const speed = this.speed; this.timePassed += deltaTime*speed; for(let i = objectsArray.length; i--;){ const object = objectsArray[i]; if(object.delete){ objectsArray.splice(i, 1); } object.update(deltaTime/1000, speed); } update(deltaTime/1000, this); }; let lastRendered = 0; this.render = (timeStamp) => { const deltaTime = timeStamp - lastRendered; lastRendered = timeStamp; if(backgroundColor) display.clear(backgroundColor); const length = objectsArray.length; for(let i = objectsArray.length; i--; ){ const object = objectsArray[length - i - 1]; if(object.draw){ const updateFillers = Object.entries(object.updateFillers); const fillersLength = updateFillers.length; if(fillersLength){ for(let i = fillersLength; i--;){ const [func, args] = updateFillers[fillersLength - i - 1]; display[func + "Rect"](object, ...args); } object.updateFillers = {}; } display.drawBuffer(object); } } const speed = this.speed; const spriteSheets = display.spriteSheets; for(let i = spriteSheets.length; i--;){ spriteSheets[i].update(deltaTime/1000*speed); } render(display, this); }; setup(this, display, this.world); this.engine = new Engine(this.frameRate, this.update, this.render, 3); this.engine.start(); return game;
}
static async create({display: {canvas, width, height, zAxis}, homeURL}){
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 96,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [
{
name: "trees",
array: true,
amount: 5,
position: {
type: "set",
positions: [ [-37.5, 37.5], [0,0], [-37.5,-37.5], [37.5,-37.5], [37.5,37.5], [10,10], [15,10], [20,10], [25,10], [30,10]]
}
}
]
},
engine: {
frameRate: 1000/30,
update: function(deltaTime, engine){
fps.innerText = 1/deltaTime;
},
render: function(display){}
},
setup: function(engine, display, {objects: {trees}}){
trees.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(trees);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* {
box-sizing:border-box;
margin:0;
padding:0;
}
body {
background-color: #111c31;
overflow: hidden;
align-items:space-around;
display:grid;
height:100%;
width:100%;
}
#canvas {
background-color: #152646;
/* justify-self: center; */
}
#fps {
position: fixed;
color: white;
right: 0;
}
canvas {
position: fixed
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>webgl x 2dCanvas</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<p id = "fps"></p>
</body>
</html>
Here's code from line 374 where the problem is happening
createBackground(objects){ //method
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {zAxis, canvas: {width, height}} = this.display;
zAxis--; //zAxis is where the camera is at, currently 96, but with webgl the objects have to be 1 point lower, so 95.
const halfZ = zAxis/2;
let {coords: [x, y], area: [w, h]} = objects[objects.length - 1];
let [mX, mY, mW, mH] = [x, y, w, h];
for(let i = objects.length-1; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
x < _x ? _x : x;
y < _y ? _y : y;
if(mX < _x){
mX = _x;
mW = _w;
}
if(mY < _y){
mY = _y;
mH = _h;
}
}
buffer.canvas.width = ((mX-halfZ+mW*2)/zAxis+1)*width;
buffer.canvas.height = ((mY-halfZ+mH*2)/zAxis+1)*height;
for(let i = objects.length; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
buffer.fillRect(((_x-halfZ-_w*2)/zAxis+1)*width, ((_y-halfZ-_h*2)/zAxis+1)*height, _w*2/zAxis*width, _h*2/zAxis*height);
}
document.body.appendChild(buffer.canvas)
}
I have this function that takes objects that are being drawn with webgl on a 3d world with a couple vectors and matrices, basically I get all their positions and volumes to draw them on a 2d canvas, heres the result I got so far
the green squares are the ones being drawn with webgl and the black squares are the ones being draw on a canvas rendering 2d, the end result should be the black squares covering the green squares but my math is off somewhere.
The full code can be found here
https://github.com/bahaaaldin214/Quixotic-Engine/tree/test
The shaders are in src/modules/webgl/shaders
other information
camera position: 96,
green squares positions:
[
[-37.5, 37.5], //bottom left
[0,0], //center
[-37.5,-37.5], //top left
[37.5,-37.5], //bottom right
[37.5,37.5], //top right
]
Well now that I've seen the code. First off, my bad but I didn't make it clear you should post minimal code. There is lots of unneeded code. Also I'm not sure if that's your own math library or if it's paired down glmatrix. If it's the latter you can just <script src="cdn/to/glmatrix"></script> to use it.
In any case you're positioning the squares using a perspective matrix and view matrix (the camera) so you need to use the same math for the 2D canvas.
const worldViewProjection = mat4.create();
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[ _w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
const EPSILON = 0.000001;
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
mat4.moveToVec3 = function(out, v) {
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
};
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity {
constructor() {
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.multiTextures = false;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount) {
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
}
class Display {
constructor(gl, programInfo, zAxis, texture) {
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]).lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
this.last = {};
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {};
}
clear(color) {
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.1, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
rect(x, y, w, h) {
const {
rect,
stroke
} = this.createRectPos(w, h);
const square = new Rect();
square.setup(...this.getRectInfo(x, y, rect, stroke));
return square;
}
fillRect(rect, color) {
const {
createStaticDrawBuffer,
gl,
parseColor
} = this;
rect.fillers.fill = true;
if (color) {
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
createRectPos(w, h) {
const rect = [w / 2, h / 2, -w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2];
const stroke = [-w / 2, h / 2, w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2, ];
return {
rect,
stroke
};
}
getRectInfo(x, y, rect, stroke) {
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {
gl,
createStaticDrawBuffer
} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length / 2];
}
createAttribInfo(numComponents, type, normalize, stride, offset) {
return {
numComponents,
type,
normalize,
stride,
offset
};
}
enableAttrib(buffer, attrib, gl, {
numComponents,
type,
normalize,
stride,
offset
}) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents, type, normalize, stride, offset);
gl.enableVertexAttribArray(attrib);
}
drawBuffer(buffer) {
const {
gl,
drawTexture,
enableAttrib,
createStaticDrawBuffer,
currentCamera,
texture: {
context,
canvas,
textAttribInfo
},
programInfo: {
uniformLocations,
program,
attribLocations: {
vertexPosition,
vertexColor,
textureCoord
}
}
} = this;
const cameraMatrix = currentCamera.matrix;
const {
positionsBuffer,
fragColorPos,
strokeColorPos,
strokePositionBuffer,
matrix,
vertexAttribInfo,
vertextColorAttribInfo,
vertexCount,
fragTextPos,
fillers: {
fill,
stroke,
texture
},
strokeSize,
textureInfo,
multiTextures
} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if (fill) {
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6) {
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Engine {
constructor(time_step, update, render, allowedSkippedFrames) {
this.accumulated_time = 0;
this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false;
this.update = update;
this.render = render;
this.allowedSkippedFrames = allowedSkippedFrames;
this.run = this.run.bind(this);
this.end = false;
}
run(time_stamp) {
const {
accumulated_time,
time,
time_step,
updated,
update,
render,
allowedSkippedFrames,
end
} = this;
this.accumulated_time += time_stamp - time;
this.time = time_stamp;
if (accumulated_time > time_stamp * allowedSkippedFrames) {
this.accumulated_time = time_stamp;
}
while (this.accumulated_time >= time_step) {
this.accumulated_time -= time_step;
update(time_stamp);
this.updated = true;
}
if (updated) {
this.updated = false;
render(time_stamp);
}
if (end) {
return;
}
this.animation_frame_request = requestAnimationFrame(this.run);
}
start() {
this.accumulated_time = this.time_step;
this.time = performance.now();
this.animation_frame_request = requestAnimationFrame(this.run);
}
stop() {
this.end = true;
cancelAnimationFrame(this.animation_frame_request);
}
}
class Entity extends Rect {
constructor() {
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args) {
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args) {
this.updateFillers.fill = args;
}
update(deltaTime, speed) {
return this;
}
move(x, y) {
super.move([x, y, this.coords[2]]);
return this;
}
}
class Quixotic {
constructor(display) {
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0;
this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args) {
const display = this.display;
const {
rect,
stroke
} = display.createRectPos(5, 5);
Class = Class ? Class : Entity;
const className = Class.name;
if (className !== "Entity" && !Entity.prototype.isPrototypeOf(Class.prototype)) {
throw new TypeError("Expected extended class of Entity. Instead got: " + className);
}
let instance;
const {
objectsArray,
classesInfo,
objects
} = this.world;
const classInfo = classesInfo[className];
if (classInfo) {
if (classInfo.args) {
instance = new Class(...[...classInfo.args, ...args]);
} else {
instance = new Class(...args);
}
const name = classInfo.name;
if (Array.isArray(objects[name])) {
objects[name].push(instance);
instance.name = name;
} else {
console.warn("Didn't save object in world.objects object, object wouldn't detect collision");
}
} else {
instance = new Class(...args);
}
instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000"));
objectsArray.push(instance);
return instance;
}
createBackground(objects) {
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {
zAxis,
canvas: {
width,
height
}
} = this.display;
zAxis--;
const halfZ = zAxis / 2;
let {
coords: [x, y],
area: [w, h]
} = objects[objects.length - 1];
const worldViewProjection = mat4.create();
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[_w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
document.body.appendChild(buffer.canvas)
}
buildWorld({
objects,
classes,
tileMap
}) {
const world = this.world;
if (Array.isArray(objects)) {
for (let i = objects.length - 1; i > -1; i--) {
const object = objects[i];
const {
name,
array,
amount,
position,
collision,
args,
area
} = object;
let createClass;
if (!object.class) {
createClass = Entity;
}
const _args = args ? args : [];
let pos;
if (position) {
let p = amount;
if (array) {
const positions = position.positions;
pos = function() {
p--;
return positions[p];
};
} else {
pos = function() {
return position.position;
};
}
}
if (array) {
let _array = [];
for (let j = amount; j--;) {
const instance = this.createEntity(createClass, ..._args);
instance.name = name;
if (position) {
instance.move(...pos());
}
if (area) {
instance.setSize(area);
}
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
}
return;
}
setup(game) {
const {
style: {
backgroundColor,
backgroundImage,
stroke
},
world,
engine: {
frameRate,
update,
render
},
setup
} = game;
this.buildWorld(world);
const {
display,
entitySystem,
world: {
objectsArray,
objects
}
} = this;
if (backgroundImage) {
display.gl.canvas.style.background = `url(${backgroundImage})`;
if (repeatX || repeatY) {
console.log("not read yet");
}
}
this.frameRate = frameRate;
let lastUpdated = 0;
this.update = (time) => {
let deltaTime = time - lastUpdated;
lastUpdated = time;
const speed = this.speed;
this.timePassed += deltaTime * speed;
for (let i = objectsArray.length; i--;) {
const object = objectsArray[i];
if (object.delete) {
objectsArray.splice(i, 1);
}
object.update(deltaTime / 1000, speed);
}
update(deltaTime / 1000, this);
};
let lastRendered = 0;
this.render = (timeStamp) => {
const deltaTime = timeStamp - lastRendered;
lastRendered = timeStamp;
if (backgroundColor) display.clear(backgroundColor);
const length = objectsArray.length;
for (let i = objectsArray.length; i--;) {
const object = objectsArray[length - i - 1];
if (object.draw) {
const updateFillers = Object.entries(object.updateFillers);
const fillersLength = updateFillers.length;
if (fillersLength) {
for (let i = fillersLength; i--;) {
const [func, args] = updateFillers[fillersLength - i - 1];
display[func + "Rect"](object, ...args);
}
object.updateFillers = {};
}
display.drawBuffer(object);
}
}
const speed = this.speed;
const spriteSheets = display.spriteSheets;
for (let i = spriteSheets.length; i--;) {
spriteSheets[i].update(deltaTime / 1000 * speed);
}
render(display, this);
};
setup(this, display, this.world);
this.engine = new Engine(this.frameRate, this.update, this.render, 3);
this.engine.start();
return game;
}
static async create({
display: {
canvas,
width,
height,
zAxis
},
homeURL
}) {
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 96,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [{
name: "trees",
array: true,
amount: 5,
position: {
type: "set",
positions: [
[-37.5, 37.5],
[0, 0],
[-37.5, -37.5],
[37.5, -37.5],
[37.5, 37.5],
[10, 10],
[15, 10],
[20, 10],
[25, 10],
[30, 10]
]
}
}]
},
engine: {
frameRate: 1000 / 30,
update: function(deltaTime, engine) {
fps.innerText = 1 / deltaTime;
},
render: function(display) {}
},
setup: function(engine, display, {
objects: {
trees
}
}) {
trees.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(trees);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #111c31;
overflow: hidden;
align-items: space-around;
display: grid;
height: 100%;
width: 100%;
}
#canvas {
background-color: #152646;
/* justify-self: center; */
}
#fps {
position: fixed;
color: white;
right: 0;
}
canvas {
position: fixed
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<canvas id="canvas" width="300" height="300"></canvas>
<p id="fps"></p>
note: the code only works because the camera is not rotated, nor are the squares. If you did rotate the camera or the squares you'd need to draw triangles with canvas 2d after transforming each set of 3 vertices, just like WebGL does.

Categories

Resources