I am trying to draw 2D metaballs using WebGL2. I render a bunch of quads with transparent radial gradient and gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) to a separate framebuffer. I then use the resulting texture in a fullscreen quad, where I decide if pixel should be rendered based on it's alpha value like so:
vec4 inputColor = texture(u_texture, v_uv);
float cutoffThreshold = 0.14;
float cutoff = step(cutoffThreshold, inputColor.a);
float threshold = 0.005;
outputColor = mix(
vec4(1, 0, 0, 1),
vec4(0, 0, 1, 1),
cutoff
);
While this kinda works, it produces really noticeable artefacts along the edges:
I think the problem lays in my blending operation. I tried enabling blending only when drawing to my framebuffer and disableing it when rendering my main quad without much success.
Here is my program:
const CONFIG = {
ballsCount: 10,
ballRadius: isMobileBrowser() ? 75 : 200,
gravity: 0.1,
lineWidth: innerWidth / 2,
startVelocityX: { min: 0, max: 0.1 },
startVelocityY: { min: 1, max: 3 },
}
const contentWrapper = document.querySelector('.content')
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl2')
const dpr = devicePixelRatio > 2.5 ? 2.5 : devicePixelRatio
if (!gl) {
showWebGL2NotSupported()
}
const lineVertexArrayObject = gl.createVertexArray()
const quadVertexArrayObject = gl.createVertexArray()
const ballsVertexArrayObject = gl.createVertexArray()
const ballsOffsetsBuffer = gl.createBuffer()
let oldTime = 0
let lineAngle = 0
// WebGL Programs
let lineWebGLProgram
let quadWebGLProgram
let ballsWebGLProgram
let quadTextureUniformLoc
let lineAngleUniformLoc
let lineVertexArray
let ballsOffsetsArray
// Not for rendering, just storing the balls velocities
let ballsVelocitiesArray
/* ------- Create horizontal line WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
uniform vec2 u_resolution;
uniform float u_angle;
in vec4 a_position;
mat4 rotationZ( in float angle ) {
return mat4(
cos(angle), -sin(angle), 0.0, 0.0,
sin(angle), cos(angle), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
void main () {
gl_Position = u_projectionMatrix * (rotationZ(u_angle) * a_position + vec4(u_resolution.xy / 2.0, 0.0, 1.0));
}
`,
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
out vec4 outputColor;
void main () {
outputColor = vec4(0, 0, 1, 1);
}
`,
})
lineWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign horizontal line WebGL attributes ------- */
{
lineVertexArray = new Float32Array([-CONFIG.lineWidth / 2, 0, CONFIG.lineWidth / 2, 0])
const vertexBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(lineWebGLProgram, 'a_position')
gl.bindVertexArray(lineVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, lineVertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindVertexArray(null)
}
/* ------- Create metaballs WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
in vec4 a_position;
in vec4 a_offsetPosition;
in vec2 a_uv;
out vec2 v_uv;
void main () {
vec4 correctOffsetedPosition = a_offsetPosition + a_position;
gl_Position = u_projectionMatrix * correctOffsetedPosition;
v_uv = a_uv;
}
`
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outputColor;
void main () {
float dist = distance(v_uv, vec2(0.5));
float c = 0.5 - dist;
outputColor = vec4(vec3(1.0), c);
}
`
})
ballsWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign metaballs WebGL attributes ------- */
{
const vertexArray = new Float32Array([
-CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2,
-CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2,
-CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2
])
const uvsArray = makeQuadUVs()
ballsOffsetsArray = new Float32Array(CONFIG.ballsCount * 2)
ballsVelocitiesArray = new Float32Array(CONFIG.ballsCount * 2)
for (let i = 0; i < CONFIG.ballsCount; i++) {
ballsOffsetsArray[i * 2 + 0] = Math.random() * innerWidth
ballsOffsetsArray[i * 2 + 1] = Math.random() * innerHeight
ballsVelocitiesArray[i * 2 + 0] = (Math.random() * 2 - 1) * CONFIG.startVelocityX.max + CONFIG.startVelocityX.min
ballsVelocitiesArray[i * 2 + 1] = Math.random() * CONFIG.startVelocityY.max + CONFIG.startVelocityY.min
}
const vertexBuffer = gl.createBuffer()
const uvsBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(ballsWebGLProgram, 'a_position')
const a_uv = gl.getAttribLocation(ballsWebGLProgram, 'a_uv')
const a_offsetPosition = gl.getAttribLocation(ballsWebGLProgram, 'a_offsetPosition')
gl.bindVertexArray(ballsVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, uvsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, uvsArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_uv)
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, ballsOffsetsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, ballsOffsetsArray, gl.DYNAMIC_DRAW)
gl.enableVertexAttribArray(a_offsetPosition)
gl.vertexAttribPointer(a_offsetPosition, 2, gl.FLOAT, false, 0, 0)
gl.vertexAttribDivisor(a_offsetPosition, 1)
gl.bindVertexArray(null)
}
/* ------- Create fullscreen quad WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
in vec4 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main () {
gl_Position = u_projectionMatrix * a_position;
v_uv = a_uv;
}
`
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
uniform sampler2D u_texture;
in vec2 v_uv;
out vec4 outputColor;
void main () {
vec4 inputColor = texture(u_texture, v_uv);
float cutoffThreshold = 0.14;
float cutoff = step(cutoffThreshold, inputColor.a);
float threshold = 0.005;
outputColor = mix(
vec4(1, 0, 0, 1),
vec4(0, 0, 1, 1),
cutoff
);
cutoffThreshold += 0.001;
cutoff = smoothstep(cutoffThreshold - threshold, cutoffThreshold + threshold, inputColor.a);
outputColor = mix(
outputColor,
vec4(1, 0, 0, 1),
cutoff
);
cutoffThreshold += 0.05;
cutoff = smoothstep(cutoffThreshold - threshold, cutoffThreshold + threshold, inputColor.a);
outputColor = mix(
outputColor,
vec4(0, 1, 0, 1),
cutoff
);
// outputColor = mix(inputColor, mix(baseColor, metaballsColor, cutoff), 0.3);
// outputColor = inputColor;
}
`
})
quadWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign fullscreen quad WebGL attributes ------- */
{
const vertexArray = new Float32Array([
0, innerHeight / 2,
innerWidth / 2, innerHeight / 2,
innerWidth / 2, 0,
0, innerHeight / 2,
innerWidth / 2, 0,
0, 0
])
const uvsArray = makeQuadUVs()
const vertexBuffer = gl.createBuffer()
const uvsBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(quadWebGLProgram, 'a_position')
const a_uv = gl.getAttribLocation(quadWebGLProgram, 'a_uv')
gl.bindVertexArray(quadVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, uvsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, uvsArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_uv)
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, 0, 0)
gl.bindVertexArray(null)
}
/* ------- Create WebGL texture to render to ------- */
gl.getExtension('EXT_color_buffer_float')
gl.getExtension('EXT_float_blend')
gl.getExtension('OES_texture_float_linear')
const targetTextureWidth = innerWidth * dpr
const targetTextureHeight = innerHeight * dpr
const targetTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, targetTextureWidth, targetTextureHeight, 0, gl.RGBA, gl.FLOAT, null)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.bindTexture(gl.TEXTURE_2D, null)
/* ------- Create WebGL framebuffer to render to ------- */
const framebuffer = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetTexture, 0)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
init()
function init () {
document.body.appendChild(canvas)
resize()
window.addEventListener('resize', resize)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// gl.blendEquation(gl.FUNC_SUBTRACT)
const projectionMatrix = makeProjectionMatrix(innerWidth / 2, innerHeight / 2)
let u_projectionMatrix
gl.useProgram(ballsWebGLProgram)
u_projectionMatrix = gl.getUniformLocation(ballsWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
gl.useProgram(null)
gl.useProgram(quadWebGLProgram)
quadTextureUniformLoc = gl.getUniformLocation(quadWebGLProgram, 'u_texture')
gl.uniform1i(quadTextureUniformLoc, 0)
u_projectionMatrix = gl.getUniformLocation(quadWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
gl.useProgram(null)
gl.useProgram(lineWebGLProgram)
u_projectionMatrix = gl.getUniformLocation(lineWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
const u_resolution = gl.getUniformLocation(lineWebGLProgram, 'u_resolution')
gl.uniform2f(u_resolution, innerWidth, innerHeight)
lineAngleUniformLoc = gl.getUniformLocation(lineWebGLProgram, 'u_angle')
gl.uniform1f(lineAngleUniformLoc, lineAngle * Math.PI / 180)
gl.useProgram(null)
requestAnimationFrame(renderFrame)
}
let a = true
document.addEventListener('click', () => {
a = !a
})
function renderFrame (ts) {
const dt = ts - oldTime
oldTime = ts
for (let i = 0; i < CONFIG.ballsCount; i++) {
ballsVelocitiesArray[i * 2 + 1] += CONFIG.gravity
ballsOffsetsArray[i * 2 + 0] += ballsVelocitiesArray[i * 2 + 0]
ballsOffsetsArray[i * 2 + 1] += ballsVelocitiesArray[i * 2 + 1]
if (ballsOffsetsArray[i * 2 + 0] < CONFIG.ballRadius / 2) {
ballsOffsetsArray[i * 2 + 0] = CONFIG.ballRadius / 2
ballsVelocitiesArray[i * 2 + 0] *= -1
}
if (ballsOffsetsArray[i * 2 + 0] > innerWidth - CONFIG.ballRadius / 2) {
ballsOffsetsArray[i * 2 + 0] = innerWidth - CONFIG.ballRadius / 2
ballsVelocitiesArray[i * 2 + 0] *= -1
}
if (ballsOffsetsArray[i * 2 + 1] - CONFIG.ballRadius > innerHeight) {
ballsOffsetsArray[i * 2 + 1] = -CONFIG.ballRadius
ballsVelocitiesArray[i * 2 + 1] = 5 + Math.random() * 3
}
}
checkLine()
gl.bindBuffer(gl.ARRAY_BUFFER, ballsOffsetsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, ballsOffsetsArray, gl.DYNAMIC_DRAW)
if (a) {
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
}
gl.viewport(0, 0, targetTextureWidth, targetTextureHeight)
gl.clearColor(0.1, 0.1, 0.1, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.bindVertexArray(ballsVertexArrayObject)
gl.useProgram(ballsWebGLProgram)
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, CONFIG.ballsCount)
gl.bindVertexArray(null)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.viewport(0, 0, canvas.width, canvas.height)
if (a) {
gl.bindVertexArray(quadVertexArrayObject)
gl.useProgram(quadWebGLProgram)
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.drawArrays(gl.TRIANGLES, 0, 6)
gl.useProgram(null)
gl.bindVertexArray(null)
gl.bindTexture(gl.TEXTURE_2D, null)
}
lineAngle = Math.sin(ts * 0.001) * 30
gl.bindVertexArray(lineVertexArrayObject)
gl.useProgram(lineWebGLProgram)
gl.uniform1f(lineAngleUniformLoc, -lineAngle * Math.PI / 180)
gl.drawArrays(gl.LINES, 0, 2)
gl.useProgram(null)
gl.bindVertexArray(null)
requestAnimationFrame(renderFrame)
}
function getLineBounds () {
const x1 = lineVertexArray[0]
const y1 = lineVertexArray[1]
const x2 = lineVertexArray[2]
const y2 = lineVertexArray[3]
if (lineAngle === 0) {
const minX = Math.min(x1, x2)
const minY = Math.min(y1, y2)
const maxX = Math.max(x1, x2)
const maxY = Math.max(y1, y2)
return {
x: x1 + minX,
y: y1 + minY,
width: maxX - minX,
height: maxY - minY,
}
} else {
const rotation = lineAngle * Math.PI / 180
const sin = Math.sin(rotation)
const cos = Math.cos(rotation)
const x1r = cos * x1 + sin * y1
const x2r = cos * x2 + sin * y2
const y1r = cos * y1 + sin * x1
const y2r = cos * y2 + sin * x2
const x = innerWidth / 2 + x1 + Math.min(x1r, x2r) + CONFIG.lineWidth / 2
const y = innerHeight / 2 + y1 + Math.min(y1r, y2r) + CONFIG.lineWidth / 2
const width = Math.max(x1r, x2r) - Math.min(x1r, x2r)
const height = Math.max(y1r, y2r) - Math.min(y1r, y2r)
return {
x,
y,
width,
height,
}
}
}
function checkLine () {
const lineBounds = getLineBounds()
const ballRadius = CONFIG.ballRadius / 7
for (let i = 0; i < CONFIG.ballsCount; i++) {
const ballx = ballsOffsetsArray[i * 2 + 0]
const bally = ballsOffsetsArray[i * 2 + 1]
const ballvx = ballsVelocitiesArray[i * 2 + 0]
const ballvy = ballsVelocitiesArray[i * 2 + 1]
if (ballx + ballRadius / 2 > lineBounds.x && ballx - ballRadius / 2 < lineBounds.x + lineBounds.width) {
const lineRotation = lineAngle * Math.PI / 180
const cos = Math.cos(lineRotation)
const sin = Math.sin(lineRotation)
let x = ballx - innerWidth / 2
let y = bally - innerHeight / 2
let vx1 = cos * ballvx + sin * ballvy
let vy1 = cos * ballvy - sin * ballvx
let y1 = cos * y - sin * x
if (y1 > -ballRadius / 2 && y1 < vy1) {
// debugger
const x2 = cos * x + sin * y
y1 = -ballRadius / 2
vy1 *= -0.45
x = cos * x2 - sin * y1
y = cos * y1 + sin * x2
ballsVelocitiesArray[i * 2 + 0] = cos * vx1 - sin * vy1
ballsVelocitiesArray[i * 2 + 1] = cos * vy1 + sin * vx1
ballsOffsetsArray[i * 2 + 0] = innerWidth / 2 + x
ballsOffsetsArray[i * 2 + 1] = innerHeight / 2 + y
}
}
}
}
function resize () {
canvas.width = innerWidth * dpr
canvas.height = innerHeight * dpr
canvas.style.width = `${innerWidth}px`
canvas.style.height = `${innerHeight}px`
}
/* ------- WebGL helpers ------- */
function makeQuadUVs () {
return new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1])
}
function makeWebglShader (gl, { shaderType, shaderSource }) {
const shader = gl.createShader(shaderType)
gl.shaderSource(shader, shaderSource)
gl.compileShader(shader)
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
if (success) {
return shader
}
console.error(`
Error in ${shaderType === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:
${gl.getShaderInfoLog(shader)}
`)
gl.deleteShader(shader)
}
function makeWebglProram (gl, { vertexShader, fragmentShader }) {
const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
const success = gl.getProgramParameter(program, gl.LINK_STATUS)
if (success) {
return program
}
console.error(gl.getProgramInfoLog(program))
gl.deleteProgram(program)
}
function makeProjectionMatrix (width, height) {
return new Float32Array([
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 0, 0,
-1, 1, 0, 1,
])
}
function showWebGL2NotSupported () {
const errorMessageWrapper = document.createElement('div')
if (isIOS()) {
const iOSVersion = getIOSVersion().major
if (iOSVersion === 13) {
errorMessageWrapper.innerHTML = `
<p>Please update your device to iOS / iPadOS 14 so you can see this demo.</p>
`
} else if (iOSVersion === 14) {
errorMessageWrapper.innerHTML = `
<p>In order to see WebGL2 content, you need to enable it from your device settings.</p>
<p>Settings > Safari > Advanced > Experimental Features > WebGL2.0</p>
`
}
} else {
errorMessageWrapper.innerHTML = `
<h1>Your browser does not support WebGL2</h1>
<p>Please try one of these alternative browsers:</p>
<ul>
<li>Microsoft Edge (version 79+)</li>
<li>Mozilla Firefox (version 51+)</li>
<li>Google Chrome (version 56+)</li>
<li>Opera (version 43+)</li>
</ul>
`
}
errorMessageWrapper.classList.add('webgl2-error')
document.body.appendChild(errorMessageWrapper)
}
/* ------- Generic helpers ------- */
function isMobileBrowser () {
return (function (a) {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) {
return true
}
return false
})(navigator.userAgent || navigator.vendor || window.opera)
}
function isIOS () {
return (/AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent)) || isIPadOS()
}
function isIPadOS () {
return navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 && !window.MSStream
}
function getIOSVersion () {
const found = navigator.userAgent.match(/(iPhone|iPad); (CPU iPhone|CPU) OS (\d+)_(\d+)(_(\d+))?\s+/)
if (!found || found.length < 4) {
return {
major: 0,
minor: 0
}
}
return {
major: parseInt(found[3], 10),
minor: parseInt(found[4], 10)
}
}
* { margin: 0; padding: 0; }
EDIT
By the first comment suggestion, I enabled the EXT_color_buffer_float and OES_texture_float_linear extensions and augmented my texImage2D call like this:
gl.getExtension('EXT_color_buffer_float')
gl.getExtension('OES_texture_float_linear')
// ...
const targetTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, targetTextureWidth, targetTextureHeight, 0, gl.RGBA, gl.FLOAT, null)
Unfortunately this results in corrupted rendering on all of my browsers running on MacOS. I updated my code snippet to reflect the new changes.
I read in MDN's Webgl2 best practices that EXT_float_blend is also required in order to allow blending in framebuffers, but is implicitly allowed when requesting EXT_color_buffer_float?
On a sidenote, I guess the OES_texture_float_linear would not work on mobile devices? So if the extension is not available, I would need to fallback to 8bit RGBA textures?
Thanks!
I'm pretty sure the issue the texture your rendering to is 8bits. Switch it to a floating point texture (RGBA32F) You'll need to check for and enable EXT_color_buffer_float and OES_texture_float_linear
Update
You say it won't work on mobile but you're using WebGL2 which hasn't shipped on iPhone yet (2021/1/3). As for RGBA32F not being renderable on mobile you could try RGBA16F. You'll have to check for and enable the corresponding extensions, EXT_color_buffer_half_float and OES_texture_half_float_linear. Your current code is not checking that the extensions actually exist (I'm assuming that was just to keep the code minimal)
The corruption is that your circle calculation draws alpha < 0 outside the circle but inside the quad. Before that was getting clipped to 0 because of the texture format but now with floating point textures it's not so it affects other circles.
Either discard if c <= 0 or clamp so it doesn't go below 0.
Note: you might find coloring faster and more flexible using a ramp texture. example, example2
Also note: It would have been nice if you'd created a more minimal repo. There's no need for the animation to show either issue
Update 2
Something else to point out, maybe you already knew this, but, the circle calculation
float dist = distance(v_uv, vec2(0.5));
float c = 0.5 - dist;
means the max value is 0.5 so half the range is being thrown away.
Switching to this
float dist = distance(v_uv, vec2(0.5)) * 2.0;
float c = 1.0 - dist;
Changes the range from 0 to 1 which is arguably better for shading later and to not throw away a bunch of precision in the texture. Example
I am drawing thousand of colored quads by using WebGL (no any framework) and on my laptop, around 80k quads moves nicely in 60fps but more than 80K quads, fps starts waving regularly. Like a few frame 30fp, one frame 60 fps. When i check it Chrome's performance tools, i noticed that GPU is taking too much time.
This is how Chrome Performance tool look like when i run 100k quads
This is my example with no moving quads. Dynamic one also has same effect but STATIC one shows my problem better since no JS overhead.
My code here:
var objects = [];
var MAX_COUNT = 10000;
var projectionMatrix;
var gl;
var positionVertexBuffer;
var colorVertexBuffer;
var indicesBuffer;
{
gl = document.getElementById("renderCanvas").getContext("webgl", {preserveDrawingBuffer: false});
gl.disable(gl.STENCIL_TEST);
gl.disable(gl.DEPTH_TEST);
document.getElementById("renderCanvas").onclick = createObjects;
createObjects();
requestAnimationFrame(updateScreen);
}
function createObjects () {
projectionMatrix = new Float32Array([
0.0033333333333333335,0,0,
0,-0.0033333333333333335,0,
0,0,1
]);
var rObject = {};
rObject.projectionMatrix = projectionMatrix;
createPrograms(rObject);
createAttributes(rObject);
createMoveObjects(rObject);
rObject.id = "id_" + objects.length ;
objects.push(rObject);
}
function createMoveObjects (outObject) {
outObject.points = [];
var k = 0;
for (var i = 0; i < MAX_COUNT; i++) {
var x = (Math.random() * 600) - 300;
var y = (Math.random() * 600) - 300;
var vx = (Math.random() * 10) - 5;
var vy = (Math.random() * 10) - 5;
var size = 30 + Math.random() * 1;
var w = 26 / 2;
var h = 37 / 2;
var p = {w:w, h:h, x:x, y:y, vx:vx, vy:vy, size:size};
outObject.points.push(p);
}
}
var shaderProgram;
function createPrograms(outObject) {
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, document.getElementById("vertexShader").textContent );
gl.compileShader(vertexShader);
if ( !gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) ) {
let finfo = gl.getShaderInfoLog( vertexShader );
console.log("Vertex Shader Fail" , finfo);
}
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, document.getElementById("fragmentShader").textContent);
gl.compileShader(fragmentShader);
if ( !gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) ) {
let finfo = gl.getShaderInfoLog( fragmentShader );
console.log("Fragment Shader Fail" , finfo);
}
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
var pmlocation = gl.getUniformLocation(shaderProgram,"projectionMatrix");
gl.useProgram(shaderProgram);
gl.uniformMatrix3fv(pmlocation, false , outObject.projectionMatrix);
outObject.projectionMatrixLocation = pmlocation;
outObject.shaderProgram = shaderProgram;
}
function createAttributes(outObject) {
var vertices = new Float32Array(MAX_COUNT * 8);
var colors = new Float32Array(MAX_COUNT * 12);
var indices = new Uint16Array(6 * MAX_COUNT);
var index = 0;
for (var i = 0; i < indices.length; i+=6) {
indices[i ] = index;
indices[i + 1] = index + 1;
indices[i + 2] = index + 2;
indices[i + 3] = index + 1;
indices[i + 4] = index + 3;
indices[i + 5] = index + 2;
index += 4;
}
var r,g,b;
for (var i = 0; i < colors.length; i+=12) {
r = Math.random();
g = Math.random();
b = Math.random();
colors[i] = r;
colors[i + 1] = g;
colors[i + 2] = b;
colors[i + 3] = r;
colors[i + 4] = g;
colors[i + 5] = b;
colors[i + 6] = r;
colors[i + 7] = g;
colors[i + 8] = b;
colors[i + 9] = r;
colors[i + 10] = g;
colors[i + 11] = b;
}
var k = 0;
var w = 26 / 2;
var h = 37 / 2;
var x,y;
for (var i = 0; i < vertices.length; i++) {
x = (Math.random() * 600) - 300;
y = (Math.random() * 600) - 300;
vertices[k] = -w + x; vertices[k + 1] = h + y;
vertices[k + 2] = -w + x; vertices[k + 3] = -h + y;
vertices[k + 4] = w + x; vertices[k + 5] = h + y;
vertices[k + 6] = w + x; vertices[k + 7] = -h + y;
k +=8;
}
positionVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
positionVertexBuffer.location = gl.getAttribLocation(shaderProgram,"position");
gl.vertexAttribPointer(positionVertexBuffer.location,2 ,gl.FLOAT, false, 0,0);
gl.enableVertexAttribArray(positionVertexBuffer.location);
colorVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
colorVertexBuffer.location = gl.getAttribLocation(shaderProgram,"color");
gl.vertexAttribPointer(colorVertexBuffer.location,3 ,gl.FLOAT, false, 0,0);
gl.enableVertexAttribArray(colorVertexBuffer.location);
indicesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
outObject.positionVertexBuffer = positionVertexBuffer;
outObject.colorVertexBuffer = colorVertexBuffer;
outObject.indicesBuffer = indicesBuffer;
outObject.vertices = vertices;
outObject.indices = indices;
outObject.colors = colors;
outObject.colorVertexLocation = colorVertexBuffer.location;
outObject.positionVertexLocation = positionVertexBuffer.location;
}
function updateAllPoints() {
var points;
var p;
for (var i = 0; i < objects.length; i++) {
points = objects[i].points;
var k = 0;
for (var j = 0; j < points.length; j++) {
p = points[j];
p.x += p.vx;
p.y += p.vy;
if(p.x >= 300){
p.x = 300;
p.vx *= -1;
} else if(p.x <= -300) {
p.x = -300;
p.vx *= -1;
} else if(p.y >= 300){
p.y = 300;
p.vy *= -1;
} else if(p.y <= -300) {
p.y = -300;
p.vy *= -1;
}
var vertices = objects[i].vertices;
vertices[k] = -p.w + p.x; vertices[k + 1] = p.h + p.y;
vertices[k + 2] = -p.w + p.x; vertices[k + 3] = -p.h + p.y;
vertices[k + 4] = p.w + p.x; vertices[k + 5] = p.h + p.y;
vertices[k + 6] = p.w + p.x; vertices[k + 7] = -p.h + p.y;
k +=8;
}
}
}
function renderScene() {
// updateAllPoints();
var totalDraw = 0;
gl.clearColor(0.3,0.3,0.3,1);
gl.clear(gl.COLOR_BUFFER_BIT);
var rO;
for (var i = 0; i < objects.length; i++) {
rO = objects[i];
drawObjects(rO);
totalDraw += MAX_COUNT;
}
document.getElementById("objectCounter").innerHTML = totalDraw + " Objects"
}
function drawObjects (rO) {
gl.useProgram(rO.shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, rO.positionVertexBuffer);
// gl.bufferSubData(gl.ARRAY_BUFFER, 0, rO.vertices);
gl.vertexAttribPointer(rO.positionVertexLocation,2 ,gl.FLOAT, false, 0,0);
gl.bindBuffer(gl.ARRAY_BUFFER, rO.colorVertexBuffer);
gl.vertexAttribPointer(rO.colorVertexLocation,3 ,gl.FLOAT, false, 0,0);
gl.drawElements(gl.TRIANGLES,MAX_COUNT * 6 , gl.UNSIGNED_SHORT, 0);
}
function updateScreen() {
if(gl){
renderScene();
requestAnimationFrame(updateScreen);
}
}
<script id="vertexShader" type="x-shader/x-vertex">
uniform mat3 projectionMatrix;
attribute vec2 position;
attribute vec3 color;
varying vec3 colorData;
void main() {
colorData = color;
vec3 newPos = vec3(position.x, position.y, 1.0 ) * projectionMatrix;
gl_Position = vec4(newPos , 1.0);
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
precision lowp float;
uniform sampler2D uSampler;
varying vec3 colorData;
void main() {
gl_FragColor = vec4(colorData, 1.0);
}
</script>
<canvas id="renderCanvas" width="200" height="200"></canvas>
<div id="objectCounter">10000 Objects</div>
<div>Evevy Click adds 10K Squares </div>
I also checked other examples and found PixiJS's Bunnymark test where you can run 120k bunnies in 60fps but no GPU overhead.
When comparing Bunnymark test, my GPU is taking too much time and I don't know why. I opimized it (of I think I did) but problem insists.
It turned out it is because of i left antialias default as context attributes which seems "true". Can't believe i did not notice.
This code worked for me
canvasDom.getContext("webgl", {antialias : false});
Use scene.remove(mesh) OR mesh.parent.remove(mesh)
I need to draw arrow of my line chart but i don't know well, here there are my code that make arrow http://jsfiddle.net/VQyVs/ I have probleme with serie 2
var lineSeries = Highcharts.seriesTypes.line;
var lineDrawGraph = lineSeries.prototype.drawGraph;
lineSeries.prototype.drawGraph = function() {
var arrowLength = 15,
arrowWidth = 9,
series = this,
segments = series.linedata || series.segments,
lastSeg = segments[segments.length - 1],
lastPoint = lastSeg[lastSeg.length - 1],
nextLastPoint = lastSeg[lastSeg.length - 2],
angle = Math.atan((lastPoint.plotX - nextLastPoint.plotX) /
(lastPoint.plotY - nextLastPoint.plotY)),
path = [];
angle = Math.PI+angle;
lineDrawGraph.apply(series, arguments);
path.push('M', lastPoint.plotX, lastPoint.plotY);
path.push(
'L',
lastPoint.plotX + arrowWidth * Math.cos(angle),
lastPoint.plotY - arrowWidth * Math.sin(angle)
);
path.push(
lastPoint.plotX + arrowLength * Math.sin(angle),
lastPoint.plotY + arrowLength * Math.cos(angle)
);
path.push(
lastPoint.plotX - arrowWidth * Math.cos(angle),
lastPoint.plotY + arrowWidth * Math.sin(angle),
'Z'
);
series.chart.renderer.path(path)
.attr({
fill: series.color
})
.add(series.group);
};
Can any one help me? Thanks
Series 2 is not sorted properly. It's putting the arrow head on the last point in the array which happens to be the first point on the X axis.
{
data: [[0.10391336,-0.647706317],
[0.208684058,-0.439022259],
[0.031920245,-0.407102014],
[-0.280249839,-0.687351853]].sort(), // I added the .sort...
marker: {
enabled: false
}
}
UPDATE
I think I understand what you are after now. To reverse the direction of the head, you'll have to test for the direction (is it moving to the left or right) and then modify how it's drawn:
if (lastPoint.plotX > nextLastPoint.plotX)
{
// to the right
path.push(
'L',
lastPoint.plotX + arrowWidth * Math.cos(angle),
lastPoint.plotY - arrowWidth * Math.sin(angle)
);
path.push(
lastPoint.plotX + arrowLength * Math.sin(angle),
lastPoint.plotY + arrowLength * Math.cos(angle)
);
path.push(
lastPoint.plotX - arrowWidth * Math.cos(angle),
lastPoint.plotY + arrowWidth * Math.sin(angle),
'Z'
);
}
else
{
// to the left
path.push(
'L',
lastPoint.plotX - arrowWidth * Math.cos(angle),
lastPoint.plotY + arrowWidth * Math.sin(angle)
);
path.push(
lastPoint.plotX - arrowLength * Math.sin(angle),
lastPoint.plotY - arrowLength * Math.cos(angle)
);
path.push(
lastPoint.plotX + arrowWidth * Math.cos(angle),
lastPoint.plotY - arrowWidth * Math.sin(angle),
'Z'
);
}
See new fiddle.
The problem is with the overriding of the original value for angle:
angle = Math.PI+angle;
Should be:
if (lastPoint.plotX > nextLastPoint.plotX){
if (angle < 0) angle = Math.PI + angle;
}
else{
if (angle > 0) angle = Math.PI + angle;
}
Mark's point about the direction of the line was correct but the proposed solution did not always work depending on the slope of the last two points of the line.
See fiddle
Ive ported over some c code that renders a sphere in opengl for a webgl/typescript project I'm working on, however its not rendering correctly. I've compared the indices and vertices between the c and ts versions and they appear to match. The code is as follows:
constructor(ctx: WebGLRenderingContext, stacks:number,
slices:number, scale: number){
var vertices: number[] = [];
var normals: number[] = [];
var indices: number[] = [];
var ii: number;
var jj: number;
var v: number;
var u: number;
normals.push(0, 0, 1);
vertices.push(0, 0, scale);
for (ii = 0; ii < slices; ++ii) {
indices.push(0);
indices.push(ii + 1);
}
indices.push(0);
indices.push(1);
for (ii = 1; ii < stacks; ++ii) {
v = ii / stacks;
for (jj = 0; jj < slices; ++jj) {
u = jj / slices;
normals.push.apply(normals, this.shapeNormal(u, v));
vertices.push.apply(vertices, this.shapeVertex(scale, u, v));
indices.push((ii - 1) * slices + (jj + 1));
var index_offset: number = ((ii + 1) === stacks) ? 0 : jj;
var second: number = ii * slices + (index_offset + 1);
//console.log("Offset: " + String(index_offset) + " Value: " + String(second));
indices.push(second);
}
indices.push((ii - 1) * slices + 1);
indices.push(ii * slices + 1);
}
normals.push(0, 0, -1);
vertices.push(0, 0, -scale);
//console.log("Theoretical vertices: " + String(3 * (2 + slices * (stacks - 1))));
//initialise vbos
console.log("Vertices: " + String(vertices.length / 3));
for(var l = 0; l < vertices.length; l += 3)
console.log(vertices[l].toFixed(6) + " " + vertices[l+1].toFixed(6) + " " + vertices[l+2].toFixed(6));
this.vertices = new VertexBufferObject(ctx, 3, vertices.length / 3);
//console.log("Normals: " + String(normals.length));
this.normals = new VertexBufferObject(ctx, 3, normals.length / 3);
console.log("Indices: " + String(indices.length) + " " + indices.toString());
this.indices = new VertexBufferObject(ctx, 1, indices.length);
//populate vbo
ctx.enableVertexAttribArray(0);
ctx.bindBuffer(ctx.ARRAY_BUFFER, this.vertices.buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(vertices), ctx.STATIC_DRAW);
ctx.enableVertexAttribArray(1);
ctx.bindBuffer(ctx.ARRAY_BUFFER, this.normals.buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(normals), ctx.STATIC_DRAW);
ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, this.indices.buffer);
ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices),
ctx.STATIC_DRAW);
ctx.bindBuffer(ctx.ARRAY_BUFFER, null);
ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null);
ctx.disableVertexAttribArray(0);
ctx.disableVertexAttribArray(1);
this.ctx = ctx;
}
private shapeVertex(r: number, u: number, v: number): number[] {
/* Use maths rather than physics spherical coordinate convention */
var theta: number = u * 2.0 * Math.PI;
var phi: number = v * Math.PI;
var vert: number[] = [
r * Math.cos(theta) * Math.sin(phi),
r * Math.sin(theta) * Math.sin(phi),
r * Math.cos(phi)
];
return vert;
}
private shapeNormal(u: number, v: number): number[] {
/* Use maths rather than physics spherical coordinate convention */
var theta: number = u * 2.0 * Math.PI;
var phi: number = v * Math.PI;
var norm: number[] = [
Math.cos(theta) * Math.sin(phi),
Math.sin(theta) * Math.sin(phi),
Math.cos(phi)
];
var mag: number = Math.sqrt(norm[0] * norm[0] + norm[1] * norm[1] + norm[2] * norm[2]);
norm[0] /= mag;
norm[1] /= mag;
norm[2] /= mag;
return norm;
}
public draw(shaderProgram: ShaderProgram): void {
//bind and draw vbo's
this.ctx.enableVertexAttribArray(0);
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vertices.buffer);
this.ctx.vertexAttribPointer(shaderProgram.attributes.position,
this.vertices.itemSize, this.ctx.FLOAT, false, 0, 0);
this.ctx.enableVertexAttribArray(1);
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.normals.buffer);
this.ctx.vertexAttribPointer(shaderProgram.attributes.normal,
this.normals.itemSize, this.ctx.FLOAT, false, 0, 0);
this.ctx.bindBuffer(this.ctx.ELEMENT_ARRAY_BUFFER, this.indices.buffer);
this.ctx.drawElements(this.ctx.TRIANGLES, this.indices.numItems,
this.ctx.UNSIGNED_SHORT, 0);
this.ctx.bindBuffer(this.ctx.ELEMENT_ARRAY_BUFFER, null);
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, null);
this.ctx.disableVertexAttribArray(0);
this.ctx.disableVertexAttribArray(1);
}
and a screenshot of the result:
Broken Sphere
Thank you in advance
As TypeScript is just a supersed of Javascript, your problem is probably related to how Javascript handle your code computations.
I'm not sure about your code as you didn't provide the original source.
Assuming your code is correct, you may encounter a floating point approximation error.