I want to graph about 120k series, each of them having 64 points (down sampled, 128-512 points if using the real sampling rate, which is even larger)
I have attempted to do it with dygraph but it seems to be very slow if i use more than 1000 series.
I have attempted to use vanilla WebGL, it drew really fast, but than my problem was getting the mouse's click and than deciding which series it was - any strategies on this? (I believe its called unprojecting?) - since there are 100k+ series, using a different color for each series and than using the click coordinate's pixel's color to determine the series is impractical. Any other strategies?
My current design draws the graph as a large PNG atlas containing all the graphs, this is fast to load, but on changes on the data i have to redraw the PNG on the server and than show it again, also "unprojecting" is an issue here - any ideas on how to solve it? if possible?
The data is already quite down sampled, further down sampling will probably result in loss of details i would like to show the end-user.
Drawing 120k * 64 things means every single pixel of a 2700x2700 could be covered. In other words you're probably trying to display too much data? It's also a very large number of things and likely to be slow.
In any case drawing and picking via WebGL is relatively easy. You draw your scene using whatever techniques you want. Then, separately, when the user clicks the mouse (or always under the mouse) you draw the entire scene again to an offscreen framebuffer giving every selectable thing a different color. Given there are 32bits of color by default (8bits red, 8bits green, 8bits blue, 8bits alpha) can count 2^32-1 things. Of course with other buffer formats you could count even higher or draw to multiple buffers but storing the data for 2^32 things is probably the larger limit.
In any case here's an example. This one makes 1000 cubes (just used cubes because this sample already existed). You can consider each cube one of of your "series" with 8 points although the code is actually drawing 24 points per cube. (set primType = gl.TRIANGLES) to see the cubes. It put all the cubes in the same buffer so that a single draw call draws all the cubes. This makes it much faster than if we draw each cube with a separate draw call.
The important part is making a series ID per series. In the code below all points of one cube have the same ID.
The code draws the scene twice. Once with each cube's color, again with each cube's ID into an offscreen texture (as a framebuffer attachment). To know which cube is under the mouse we look up the pixel under the mouse, convert its color back into an ID and update that cube's vertex colors to highlight it.
const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;
const renderVS = `
attribute vec4 position;
attribute vec4 color;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_color;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_color = color;
}
`;
const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
const idVS = `
attribute vec4 position;
attribute vec4 id;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_id;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_id = id; // pass the id to the fragment shader
}
`;
const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
gl_FragColor = v_id;
}
`;
// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);
// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;
// adapted from http://stackoverflow.com/a/26127012/128511
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
const rnd = 1.;
const offset = 2. / samples;
const increment = Math.PI * (3. - Math.sqrt(5.));
// for i in range(samples):
const y = ((i * offset) - 1.) + (offset / 2.);
const r = Math.sqrt(1. - Math.pow(y ,2.));
const phi = ((i + rnd) % samples) * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return [x, y, z];
}
const addCubeVertexData = (function() {
const CUBE_FACE_INDICES = [
[3, 7, 5, 1], // right
[6, 2, 0, 4], // left
[6, 7, 3, 2], // ??
[0, 1, 5, 4], // ??
[7, 6, 4, 5], // front
[2, 3, 1, 0], // back
];
const cornerVertices = [
[-1, -1, -1],
[+1, -1, -1],
[-1, +1, -1],
[+1, +1, -1],
[-1, -1, +1],
[+1, -1, +1],
[-1, +1, +1],
[+1, +1, +1],
];
const faceNormals = [
[+1, +0, +0],
[-1, +0, +0],
[+0, +1, +0],
[+0, -1, +0],
[+0, +0, +1],
[+0, +0, -1],
];
const quadIndices = [0, 1, 2, 0, 2, 3];
return function addCubeVertexData(id, matrix, color) {
for (let f = 0; f < 6; ++f) {
const faceIndices = CUBE_FACE_INDICES[f];
for (let v = 0; v < 6; ++v) {
const ndx = faceIndices[quadIndices[v]];
const position = cornerVertices[ndx];
const normal = faceNormals[f];
positions.push(...m4.transformPoint(matrix, position));
normals.push(...m4.transformDirection(matrix, normal));
colors.push(color);
ids.push(id);
timeStamps.push(-1000);
}
}
};
}());
for (let i = 0; i < numCubes; ++i) {
const direction = fibonacciSphere(numCubes, i);
const cubePosition = v3.mulScalar(direction, radius);
const target = [0, 0, 0];
const up = [0, 1, 0];
const matrix = m4.lookAt(cubePosition, target, up);
const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
cubeColors.push(color);
addCubeVertexData(i + 1, matrix, color);
}
const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);
// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
// we know each cube uses 36 vertices. If each model was different
// we need to save the offset and number of vertices for each model
const numVertices = 36;
const offset = (id - 1) * numVertices;
colorData.fill(color, offset, offset + numVertices);
}
function setCubeTimestamp(id, timeStamp) {
const numVertices = 36;
const offset = (id - 1) * numVertices;
timeStampData.fill(timeStamp, offset, offset + numVertices);
}
// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: positions,
normal: normals,
color: new Uint8Array(colorData.buffer),
// the colors are stored as 32bit unsigned ints
// but we want them as 4 channel 8bit RGBA values
id: {
numComponents: 4,
data: new Uint8Array((new Uint32Array(ids)).buffer),
},
timeStamp: {
numComponents: 1,
data: timeStampData,
},
});
const lightDir = v3.normalize([3, 5, 10]);
// creates an RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl);
// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;
function getIdAtPixel(x, y, projection, view, time) {
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, fbi);
// no reason to render 100000s of pixels when
// we're only going to read one
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, 1, 1);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
drawCubes(idProgramInfo, projection, view, time);
gl.disable(gl.SCISSOR_TEST);
const idPixel = new Uint8Array(4);
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
// convert from RGBA back into ID.
const id = (idPixel[0] << 0) +
(idPixel[1] << 8) +
(idPixel[2] << 16) +
(idPixel[3] << 24);
return id;
}
function drawCubes(programInfo, projection, modelView, time) {
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_projection: projection,
u_modelView: modelView, // drawing at origin so model is identity
});
gl.drawArrays(primType, 0, bufferInfo.numElements);
}
function render(time) {
time *= 0.001;
++frameCount;
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// resizes the texture and depth renderbuffer to
// match the new size of the canvas.
twgl.resizeFramebufferInfo(gl, fbi);
}
const fov = Math.PI * .35;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 1000;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const radius = 45;
const angle = time * .2;
const eye = [
Math.cos(angle) * radius,
0,
Math.sin(angle) * radius,
];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
if (lastHighlightedCubeId > 0) {
// restore the last highlighted cube's color
setCubeColor(
lastHighlightedCubeId,
cubeColorsAsUint32[lastHighlightedCubeId]);
lastHighlightedCubeId = -1;
}
{
const x = mousePos.x;
const y = gl.canvas.height - mousePos.y - 1;
highlightedCubeId = getIdAtPixel(x, y, projection, view, time);
}
if (highlightedCubeId > 0) {
const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
setCubeColor(highlightedCubeId, color);
setCubeTimestamp(highlightedCubeId, time);
lastHighlightedCubeId = highlightedCubeId;
}
highlightedCubeId = Math.random() * numCubes | 0;
// NOTE: We could use `gl.bufferSubData` and just upload
// the portion that changed.
// upload cube color data.
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
// upload the timestamp
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, null);
gl.enable(gl.DEPTH_TEST);
drawCubes(renderProgramInfo, projection, view, time);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getRelativeMousePosition(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
target = target || event.target;
const pos = getRelativeMousePosition(event, target);
pos.x = pos.x * target.width / target.clientWidth;
pos.y = pos.y * target.height / target.clientHeight;
return pos;
}
gl.canvas.addEventListener('mousemove', (event, target) => {
mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
The code above uses an offscreen framebuffer the same size as the canvas but it uses the scissor test to only draw a single pixel (the one under the mouse). It would still run without the scissor test it would just be slower.
We could also make it work using just a single pixel offscreen framebuffer and using projection math so things work out.
const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;
const renderVS = `
attribute vec4 position;
attribute vec4 color;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_color;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_color = color;
}
`;
const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
const idVS = `
attribute vec4 position;
attribute vec4 id;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_id;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_id = id; // pass the id to the fragment shader
}
`;
const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
gl_FragColor = v_id;
}
`;
// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);
// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;
// adapted from http://stackoverflow.com/a/26127012/128511
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
const rnd = 1.;
const offset = 2. / samples;
const increment = Math.PI * (3. - Math.sqrt(5.));
// for i in range(samples):
const y = ((i * offset) - 1.) + (offset / 2.);
const r = Math.sqrt(1. - Math.pow(y ,2.));
const phi = ((i + rnd) % samples) * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return [x, y, z];
}
const addCubeVertexData = (function() {
const CUBE_FACE_INDICES = [
[3, 7, 5, 1], // right
[6, 2, 0, 4], // left
[6, 7, 3, 2], // ??
[0, 1, 5, 4], // ??
[7, 6, 4, 5], // front
[2, 3, 1, 0], // back
];
const cornerVertices = [
[-1, -1, -1],
[+1, -1, -1],
[-1, +1, -1],
[+1, +1, -1],
[-1, -1, +1],
[+1, -1, +1],
[-1, +1, +1],
[+1, +1, +1],
];
const faceNormals = [
[+1, +0, +0],
[-1, +0, +0],
[+0, +1, +0],
[+0, -1, +0],
[+0, +0, +1],
[+0, +0, -1],
];
const quadIndices = [0, 1, 2, 0, 2, 3];
return function addCubeVertexData(id, matrix, color) {
for (let f = 0; f < 6; ++f) {
const faceIndices = CUBE_FACE_INDICES[f];
for (let v = 0; v < 6; ++v) {
const ndx = faceIndices[quadIndices[v]];
const position = cornerVertices[ndx];
const normal = faceNormals[f];
positions.push(...m4.transformPoint(matrix, position));
normals.push(...m4.transformDirection(matrix, normal));
colors.push(color);
ids.push(id);
timeStamps.push(-1000);
}
}
};
}());
for (let i = 0; i < numCubes; ++i) {
const direction = fibonacciSphere(numCubes, i);
const cubePosition = v3.mulScalar(direction, radius);
const target = [0, 0, 0];
const up = [0, 1, 0];
const matrix = m4.lookAt(cubePosition, target, up);
const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
cubeColors.push(color);
addCubeVertexData(i + 1, matrix, color);
}
const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);
// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
// we know each cube uses 36 vertices. If each model was different
// we need to save the offset and number of vertices for each model
const numVertices = 36;
const offset = (id - 1) * numVertices;
colorData.fill(color, offset, offset + numVertices);
}
function setCubeTimestamp(id, timeStamp) {
const numVertices = 36;
const offset = (id - 1) * numVertices;
timeStampData.fill(timeStamp, offset, offset + numVertices);
}
// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: positions,
normal: normals,
color: new Uint8Array(colorData.buffer),
// the colors are stored as 32bit unsigned ints
// but we want them as 4 channel 8bit RGBA values
id: {
numComponents: 4,
data: new Uint8Array((new Uint32Array(ids)).buffer),
},
timeStamp: {
numComponents: 1,
data: timeStampData,
},
});
const lightDir = v3.normalize([3, 5, 10]);
// creates an 1x1 pixel RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl, [
{ format: gl.RGBA, type: gl.UNSIGNED_BYTE, minMag: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
{ format: gl.DEPTH_STENCIL, },
], 1, 1);
// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;
function getIdAtPixel(x, y, projectionInfo, view, time) {
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, fbi);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
drawCubes(idProgramInfo, projectionInfo, {
totalWidth: gl.canvas.width,
totalHeight: gl.canvas.height,
partWidth: 1,
partHeight: 1,
partX: x,
partY: y,
}, view, time);
const idPixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
// convert from RGBA back into ID.
const id = (idPixel[0] << 0) +
(idPixel[1] << 8) +
(idPixel[2] << 16) +
(idPixel[3] << 24);
return id;
}
function drawCubes(programInfo, projectionInfo, partInfo, modelView, time) {
const projection = projectionForPart(projectionInfo, partInfo);
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_projection: projection,
u_modelView: modelView, // drawing at origin so model is identity
});
gl.drawArrays(primType, 0, bufferInfo.numElements);
}
function projectionForPart(projectionInfo, partInfo) {
const {fov, zNear, zFar} = projectionInfo;
const {
totalWidth,
totalHeight,
partX,
partY,
partWidth,
partHeight,
} = partInfo;
const aspect = totalWidth / totalHeight;
// corners at zNear for total image
const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
const zNearTotalBottom = -zNearTotalTop;
const zNearTotalLeft = zNearTotalBottom * aspect;
const zNearTotalRight = zNearTotalTop * aspect;
// width, height at zNear for total image
const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;
const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth; const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;
return m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);
}
function render(time) {
time *= 0.001;
++frameCount;
twgl.resizeCanvasToDisplaySize(gl.canvas);
const projectionInfo = {
fov: Math.PI * .35,
zNear: 0.1,
zFar: 1000,
};
const radius = 45;
const angle = time * .2;
const eye = [
Math.cos(angle) * radius,
0,
Math.sin(angle) * radius,
];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
if (lastHighlightedCubeId > 0) {
// restore the last highlighted cube's color
setCubeColor(
lastHighlightedCubeId,
cubeColorsAsUint32[lastHighlightedCubeId]);
lastHighlightedCubeId = -1;
}
{
const x = mousePos.x;
const y = gl.canvas.height - mousePos.y - 1;
highlightedCubeId = getIdAtPixel(x, y, projectionInfo, view, time);
}
if (highlightedCubeId > 0) {
const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
setCubeColor(highlightedCubeId, color);
setCubeTimestamp(highlightedCubeId, time);
lastHighlightedCubeId = highlightedCubeId;
}
highlightedCubeId = Math.random() * numCubes | 0;
// NOTE: We could use `gl.bufferSubData` and just upload
// the portion that changed.
// upload cube color data.
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
// upload the timestamp
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, null);
gl.enable(gl.DEPTH_TEST);
drawCubes(renderProgramInfo, projectionInfo, {
totalWidth: gl.canvas.width,
totalHeight: gl.canvas.height,
partWidth: gl.canvas.width,
partHeight: gl.canvas.height,
partX: 0,
partY: 0,
}, view, time);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getRelativeMousePosition(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
target = target || event.target;
const pos = getRelativeMousePosition(event, target);
pos.x = pos.x * target.width / target.clientWidth;
pos.y = pos.y * target.height / target.clientHeight;
return pos;
}
gl.canvas.addEventListener('mousemove', (event, target) => {
mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
note that drawing POINTS in WebGL is generally slower than drawing 2 TRIANGLES of the same size. If I set the number of cubes to 100k and set primType to TRIANGLES it draws 100k cubes. On my integrated GPU the snippet window it runs at about 10-20fps. Of course with that many cubes it's impossible to pick one. If I set the radius to 250 I can at least see the picking is still working.
So as the title states I'd like to know how to plot randomly generated meshes at the y-position that matches the terrain's corresponding y-position in three.js. I've looked through the docs and feel like using a raycaster might work, but I can only see examples that uses the detection as a mouse event, and not before render, so I'm not sure how to implement it.
Here is my code for the terrain, heightmap, and mesh plotting so far. It all works technically, but as you can see the plotAssets meshes y-positions are just sitting at zero right now. Any insights would be very much appreciated, I'm pretty new to three.js.
Terrain:
var heightmaploader = new THREE.ImageLoader();
heightmaploader.load(
"assets/noisemaps/cloud.png",
function(img) {
data = getHeightData(img);
var terrainG = new THREE.PlaneBufferGeometry(700, 700, worldWidth - 1, worldDepth - 1);
terrainG.rotateX(-Math.PI / 2);
var vertices = terrainG.attributes.position.array;
for (var i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) {
vertices[j + 1] = data[i] * 5;
}
terrainG.computeFaceNormals();
terrainG.computeVertexNormals();
var material = new THREE.MeshLambertMaterial({
map: terrainT,
//side: THREE.DoubleSide,
color: 0xffffff,
transparent: false,
});
terrain = new THREE.Mesh(terrainG, material);
terrain.receiveShadow = true;
terrain.castShadow = true;
terrain.position.y = 0;
scene.add(terrain);
plotAsset('boulder-photo-01.png', 30, 18, data);
plotAsset('boulder-outline-01.png', 20, 20, data);
plotAsset('birch-outline-01.png', 10, 50, data);
plotAsset('tree-photo-01.png', 20, 50, data);
plotAsset('grass-outline-01.png', 10, 20, data);
plotAsset('grass-outline-02.png', 10, 20, data);
}
);
Plot Assets:
function plotAsset(texturefile, amount, size, array) {
console.log(array);
var loader = new THREE.TextureLoader();
loader.load(
"assets/textures/objects/" + texturefile,
function(texturefile) {
var geometry = new THREE.PlaneGeometry(size, size, 10, 1);
var material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
map: texturefile,
side: THREE.DoubleSide,
transparent: true,
depthWrite: false,
depthTest: false,
alphaTest: 0.5,
});
var uniforms = { texture: { value: texturefile } };
var vertexShader = document.getElementById( 'vertexShaderDepth' ).textContent;
var fragmentShader = document.getElementById( 'fragmentShaderDepth' ).textContent;
// add bunch o' stuff
for (var i = 0; i < amount; i++) {
var scale = Math.random() * (1 - 0.8 + 1) + 0.8;
var object = new THREE.Mesh(geometry, material);
var x = Math.random() * 400 - 400 / 2;
var z = Math.random() * 400 - 400 / 2;
object.rotation.y = 180 * Math.PI / 180;
//object.position.y = size * scale / 2;
object.position.x = x;
object.position.z = z;
object.position.y = 0;
object.castShadow = true;
object.scale.x = scale; // random scale
object.scale.y = scale;
object.scale.z = scale;
scene.add(object);
object.customDepthMaterial = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide
} );
}
}
);
}
Height Data:
function getHeightData(img) {
var canvas = document.createElement('canvas');
canvas.width = 2048 / 8;
canvas.height = 2048 / 8;
var context = canvas.getContext('2d');
var size = 2048 / 8 * 2048 / 8,
data = new Float32Array(size);
context.drawImage(img, 0, 0);
for (var i = 0; i < size; i++) {
data[i] = 0
}
var imgd = context.getImageData(0, 0, 2048 / 8, 2048 / 8);
var pix = imgd.data;
var j = 0;
for (var i = 0, n = pix.length; i < n; i += (4)) {
var all = pix[i] + pix[i + 1] + pix[i + 2];
data[j++] = all / 40;
}
return data;
}
Yes, using of THREE.Raycaster() works well.
A raycaster has the .set(origin, direction) method. The only thing you have to do here is to set the point of origin higher than the highest point of the height map.
var n = new THREE.Mesh(...); // the object we want to aling along y-axis
var collider = new THREE.Raycaster();
var shiftY = new THREE.Vector3();
var colliderDir = new THREE.Vector3(0, -1, 0); // down along y-axis to the mesh of height map
shiftY.set(n.position.x, 100, n.position.z); // set the point of the origin
collider.set(shiftY, colliderDir); //set the ray of the raycaster
colliderIntersects = collider.intersectObject(plane); // plane is the mesh of height map
if (colliderIntersects.length > 0){
n.position.y = colliderIntersects[0].point.y; // set the position of the object
}
jsfiddle example
I am able to draw a circle
I want to pick a arbitary point on circle and draw a shape like a triangle or
a simple point of the circumference
Now what I am understanding is vertexData array has that points
so I can pick a point from vertexData
But, how do I proceed with drawing the point to that location
If its only about drawing a point on canvas
I understand that in vertexShader I can
declare
attribute vec4 a_Position
and then gl_Position = a_Position
but on circumference of circle I am not understanding
please guide here
Thanks
<script>
var vertexShaderText = [
'uniform vec2 u_resolution;',
'',
'attribute vec2 a_position;',
'',
'void main()',
'{',
'',
'vec2 clipspace = a_position / u_resolution * 1.0 ;',
'',
'gl_Position = vec4(clipspace * vec2(1, -1), 0, 1);',
'}'
].join("\n");
var fragmentShaderText = [
'precision mediump float;',
'',
'void main(void)',
'{',
'',
'gl_FragColor = vec4(1.0, 0, 0, 0);',
'',
'}'
].join("\n");
var uni = function(){
var canvas = document.getElementById("game-surface");
var gl = canvas.getContext("webgl",{antialias: true});
console.log("This is working");
gl.clearColor(0.412,0.412,0.412,1);
gl.clear(gl.COLOR_BUFFER_BIT);
var vertextShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertextShader,vertexShaderText);
gl.shaderSource(fragmentShader,fragmentShaderText);
gl.compileShader(vertextShader);
gl.compileShader(fragmentShader);
if(!gl.getShaderParameter(vertextShader,gl.COMPILE_STATUS)){
console.error("Error with vertexshader",gl.getShaderInfoLog(vertextShader));
return;
}
if(!gl.getShaderParameter(fragmentShader,gl.COMPILE_STATUS)){
console.error("Error with fragmentShader",gl.getShaderInfoLog(fragmentShader));
return;
}
var program =gl.createProgram();
gl.attachShader(program,vertextShader);
gl.attachShader(program,fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
if(!gl.getProgramParameter(program,gl.LINK_STATUS)){
console.error("Error linking program",gl.getProgramInfoLog(program));
return;
}
gl.validateProgram(program);
if(!gl.getProgramParameter(program,gl.VALIDATE_STATUS)){
console.error("Error validating",gl.getProgramInfoLog(program));
}
var circle = {x: 0, y:0, r: 500};
var ATTRIBUTES = 2;
var numFans = 64;
var degreePerFan = (2* Math.PI) / numFans;
var vertexData = [circle.x, circle.y];
// console.log(gl_Position)
for(var i = 0; i <= numFans; i++) {
var index = ATTRIBUTES * i + 2; // there is already 2 items in array
var angle = degreePerFan * (i+0.1);
//console.log(angle)
vertexData[index] = circle.x + Math.cos(angle) * circle.r;
vertexData[index + 1] = circle.y + Math.sin(angle) * circle.r;
}
//console.log(vertexData);
var vertexDataTyped = new Float32Array(vertexData);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexDataTyped, gl.STATIC_DRAW);
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
gl.enableVertexAttribArray(positionLocation);
var positionLocation = gl.getAttribLocation(program, "a_position");
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, vertexData.length/ATTRIBUTES);
};
uni();
</script>
You're using a triangle fan to draw this circle, so drawing an additional shape requires a second draw call. This isn't going to scale well, since draw calls are expensive, more likely you're going to want some method to draw multiple shapes in a single draw call.
That said, as a simple example, you can add the following code to the bottom of your uni function, after the end of the first draw call at the end of the existing function, to place a second, smaller circle on the circumference of the first one using a second draw call. Given your fragment shader, this will also be a red circle, so you may want to modify the shader to use a different color.
// Insert this code at the end of the uni() function, it will make
// use of variables and GL state already declared earlier in that function.
// Pick a point along circumference, range 1 to 63
var selectedPointIndex = 8;
circle.x = vertexData[selectedPointIndex * 2];
circle.y = vertexData[selectedPointIndex * 2 + 1];
circle.r = 50;
vertexData = [circle.x, circle.y];
for(var i = 0; i <= numFans; i++) {
var index = ATTRIBUTES * i + 2; // there is already 2 items in array
var angle = degreePerFan * (i+0.1);
vertexData[index] = circle.x + Math.cos(angle) * circle.r;
vertexData[index + 1] = circle.y + Math.sin(angle) * circle.r;
}
vertexDataTyped = new Float32Array(vertexData);
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexDataTyped, gl.STATIC_DRAW);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, vertexData.length/ATTRIBUTES);