Data corruption when replacing a GLSL constant with a uniform value - gpgpu

Follow up to this recent question.
I am doing GPGPU programming in WebGL2, and I'm passing in a large 4-dimensional square array to my shaders by packing it into a texture to bypass the uniform count limits. Having freed myself from having to use a relatively small fixed-size array, I would like to be able to specify the size of the data that is actually being passed in programmatically.
Previously, I had hard-coded the size of the data to read using a const int as follows:
const int SIZE = 5;
const int SIZE2 = SIZE*SIZE;
const int SIZE3 = SIZE2*SIZE;
uniform sampler2D u_map;
int get_cell(vec4 m){
ivec4 i = ivec4(mod(m,float(SIZE)));
float r = texelFetch(u_map, ivec2(i.x*SIZE3+i.y*SIZE2+i.z*SIZE+i.w, 0), 0).r;
return int(r * 255.0);
}
If I update SIZE2 and SIZE3 to be non-constant and initialized in main, it still works:
const int SIZE = 5;
int SIZE2;
int SIZE3;
uniform sampler2D u_map;
int get_cell(vec4 m){
ivec4 i = ivec4(mod(m,float(SIZE)));
float r = texelFetch(u_map, ivec2(i.x*SIZE3+i.y*SIZE2+i.z*SIZE+i.w, 0), 0).r;
return int(r * 255.0);
}
...
void main(){
SIZE2 = SIZE*SIZE;
SIZE3 = SIZE*SIZE2;
...
}
However, if I then replace const int SIZE = 5; with uniform int SIZE;, and then add
const size_loc = gl.getUniformLocation(program, "SIZE");
gl.uniform1i(size_loc, 5);
to the JavaScript side to set it to the same integer value that used to be hardcoded, I start seeing incorrect values being read from the texture. What am I doing wrong?
UPDATE 1: I did a little experiment where I keep the constant SIZE specification, but then also pass in a uniform int alongside it. If they are not equal, I have the shader bail out and return all zeroes. This way, I could verify that the correct integer values are in fact being set on the uniform variable--but if I then make SIZE non-constant, and set it to the value of the uniform variable with which it was just compared and found to be equal then things break. What the heck?
UPDATE 2:
This works:
int SIZE = 5;
uniform int u_size;
....
void main() {
if (u_size != SIZE) return;
SIZE = u_size;
...
}
This doesn't:
int SIZE = 5;
uniform int u_size;
....
void main() {
SIZE = u_size;
...
}

I'm not able to reproduce your issue. Post a minimal, complete, verifiable, example in a snippet
Here's a working example
const vs = `#version 300 es
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision highp float;
uniform ivec4 cell;
uniform int SIZE;
int SIZE2;
int SIZE3;
uniform highp isampler2D u_map;
int get_cell(ivec4 m){
ivec4 i = m % SIZE;
int r = texelFetch(u_map, ivec2(i.x*SIZE3 + i.y*SIZE2 + i.z*SIZE + i.w, 0), 0).r;
return r;
}
out int result;
void main(){
SIZE2 = SIZE*SIZE;
SIZE3 = SIZE*SIZE2;
result = get_cell(cell);
}
`;
const gl = document.createElement('canvas').getContext('webgl2');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make a 1x1 R32I texture and attach to framebuffer
const framebufferInfo = twgl.createFramebufferInfo(gl, [
{ internalFormat: gl.R32I, minMag: gl.NEAREST, },
], 1, 1);
const size = 5;
const totalSize = size * size * size * size;
const data = new Int32Array(totalSize);
for (let i = 0; i < data.length; ++i) {
data[i] = 5 + i * 3;
}
// create a size*size*size*size by 1
// R32I texture
const tex = twgl.createTexture(gl, {
width: totalSize,
src: data,
minMag: gl.NEAREST,
internalFormat: gl.R32I,
});
gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferInfo.framebuffer);
gl.viewport(0, 0, 1, 1);
gl.useProgram(programInfo.program);
const result = new Int32Array(1);
for (let w = 0; w < size; ++w) {
for (let z = 0; z < size; ++z) {
for (let y = 0; y < size; ++y) {
for (let x = 0; x < size; ++x) {
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
cell: [x, y, z, w],
u_map: tex,
SIZE: size,
});
gl.drawArrays(gl.POINTS, 0, 1); // draw 1 point
gl.readPixels(0, 0, 1, 1, gl.RED_INTEGER, gl.INT, result);
log(x, y, z, w, ':', result[0], data[x * size * size * size + y * size * size + z * size + w]);
}
}
}
}
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
trying with the code you posted I see no issues either
const vs = `#version 300 es
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision highp float;
uniform vec4 cell;
uniform int SIZE;
int SIZE2;
int SIZE3;
uniform sampler2D u_map;
int get_cell(vec4 m){
ivec4 i = ivec4(mod(m,float(SIZE)));
float r = texelFetch(u_map, ivec2(i.x*SIZE3+i.y*SIZE2+i.z*SIZE+i.w, 0), 0).r;
return int(r * 255.0);
}
out float result;
void main(){
SIZE2 = SIZE*SIZE;
SIZE3 = SIZE*SIZE2;
// output to texture is normalized float
result = float(get_cell(cell)) / 255.0;
}
`;
const gl = document.createElement('canvas').getContext('webgl2');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const size = 5;
const totalSize = size * size * size * size;
const data = new Uint8Array(totalSize);
for (let i = 0; i < data.length; ++i) {
data[i] = (5 + i * 3) % 256;
}
// create a size*size*size*size by 1
// R8 texture
const tex = twgl.createTexture(gl, {
width: totalSize,
src: data,
minMag: gl.NEAREST,
internalFormat: gl.R8,
});
gl.viewport(0, 0, 1, 1);
gl.useProgram(programInfo.program);
const result = new Uint8Array(4);
for (let w = 0; w < size; ++w) {
for (let z = 0; z < size; ++z) {
for (let y = 0; y < size; ++y) {
for (let x = 0; x < size; ++x) {
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
cell: [x, y, z, w],
u_map: tex,
SIZE: size,
});
gl.drawArrays(gl.POINTS, 0, 1); // draw 1 point
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
log(x, y, z, w, ':', result[0], data[x * size * size * size + y * size * size + z * size + w]);
}
}
}
}
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
Note that I wouldn't use a 1 dimensional texture since there is a limit on dimensions. I'd use a 3 dimensional texture to increase the limit
const vs = `#version 300 es
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision highp float;
uniform ivec4 cell;
uniform int SIZE;
uniform highp isampler3D u_map;
int get_cell(ivec4 m){
// no idea why you made x major
ivec4 i = m % SIZE;
int r = texelFetch(
u_map,
ivec3(
i.z * SIZE + i.w,
i.yx),
0).r;
return r;
}
out int result;
void main(){
result = get_cell(cell);
}
`;
const gl = document.createElement('canvas').getContext('webgl2');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make a 1x1 R32I texture and attach to framebuffer
const framebufferInfo = twgl.createFramebufferInfo(gl, [
{ internalFormat: gl.R32I, minMag: gl.NEAREST, },
], 1, 1);
const size = 5;
const totalSize = size * size * size * size;
const data = new Int32Array(totalSize);
for (let i = 0; i < data.length; ++i) {
data[i] = 5 + i * 3;
}
// create a size*size*size*size by 1
// R32I texture 3D
const tex = twgl.createTexture(gl, {
target: gl.TEXTURE_3D,
width: size * size,
height: size,
src: data,
minMag: gl.NEAREST,
internalFormat: gl.R32I,
});
gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferInfo.framebuffer);
gl.viewport(0, 0, 1, 1);
gl.useProgram(programInfo.program);
const result = new Int32Array(1);
for (let w = 0; w < size; ++w) {
for (let z = 0; z < size; ++z) {
for (let y = 0; y < size; ++y) {
for (let x = 0; x < size; ++x) {
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
cell: [x, y, z, w],
u_map: tex,
SIZE: size,
});
gl.drawArrays(gl.POINTS, 0, 1); // draw 1 point
gl.readPixels(0, 0, 1, 1, gl.RED_INTEGER, gl.INT, result);
log(x, y, z, w, ':', result[0], data[x * size * size * size + y * size * size + z * size + w]);
}
}
}
}
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

Related

problem the display of processing IDE, the after it run, the texts are gone

IDK what just happen, but when the program is still running, the text the after it showed it gone, or it just showed one text.
can u guys help me
sorry for my bad English, I'll be appreciated your help thank you so much
this is my code
int j;
float timeinterval;
float lasttimecheck;
float endY = 0;
int index = 0;
int x2;
int y2;
int x0=150;
int y0=0;
float r = random(1, 20);
float x1 = 1;
float y1 = 1;
String[] words;
void timecapture() {
lasttimecheck = millis();
timeinterval = 0; //2sec
}
void setup() {
size (400, 400);
timecapture();
frameRate(5);
stroke(0);
}
void draw() {
background(255);
textSize(20);
int y2 = 0;
float [] x = new float [j];
float [] y = new float [j];
for (int i = 0; (i<x.length) && (i<y.length); i++ ) {
x[i] = x1;
y[i] = y1;
fill(0);
//text (x[i]+","+y[i], 20, y1);
}
y2 = y2+40;
String[] data = new String[x.length];
for (int ii = 0; ii<x.length; ii++) {
data [ii] = str(x[ii]) + ("\t") + str(y[ii]);
}
if (millis() > lasttimecheck + timeinterval) {
saveStrings("location", data);
lasttimecheck = millis();
}
if (x.length<30 && y.length<30)
j=j+1;
x1=x1+r; y1=y1+r;
}
The issue is that the arrays x and y are recreated in every frame.
void draw() {
background(255);
// [...]
float [] x = new float [j];
float [] y = new float [j];
Add global variables x, y and data. Use append() to add values to the arrays. And draw the text in a loop:
float [] x = new float [0];
float [] y = new float [0];
String[] data = new String[0];
void draw() {
background(255);
textSize(20);
for (int i = 0; (i<x.length) && (i<y.length); i++ ) {
fill(0);
text (x[i]+","+y[i], 20, i*20 + 20);
}
if (x.length <= j) {
x = append(x, x1);
y = append(y, y1);
data = append(data, str(x1) + ("\t") + str(y1));
}
if (millis() > lasttimecheck + timeinterval) {
saveStrings("location", data);
lasttimecheck = millis();
}
if (x.length<30 && y.length<30) {
j=j+1;
}
x1=x1+r; y1=y1+r;
}

OpenGL ES 2.0 - Fisheye shader displays a grey image

I've been trying to use a fisheye shader from Shadertoy.
I've added my own frame resolution, and changed some keywords (texture -> texture2D, fragColor -> gl_FragColor) but that's it.
I don't really know why it doesn't work and how to debug it..
As a result I get a unicolor grey image.
Here's the code of my fragment shader :
precision mediump float;
uniform vec4 v_Color;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
#define RESOLUTION_WIDTH 375.0
#define RESOLUTION_HEIGHT 211.0
#define POWER 2.0
void main() {
vec2 fragCoord = v_TexCoordinate;
vec2 iResolution = vec2(RESOLUTION_WIDTH, RESOLUTION_HEIGHT);
vec2 p = fragCoord.xy / iResolution.x; // normalized coords with some cheat
float prop = iResolution.x / iResolution.y;
vec2 m = vec2(0.5, 0.5 / prop); // center coords
vec2 d = p - m; // vector from center to current fragment
float r = sqrt(dot(d, d)); // distance of pixel from center
float power = POWER;
float bind; // radius of 1:1 effect
if (power > 0.0)
bind = sqrt(dot(m, m)); // stick to corners
else {
if (prop < 1.0)
bind = m.x;
else
bind = m.y;
} // stick to borders
// Weird formulas
vec2 uv;
if (power > 0.0) // fisheye
uv = m + normalize(d) * tan(r * power) * bind / tan( bind * power);
else if (power < 0.0) // antifisheye
uv = m + normalize(d) * atan(r * -power * 10.0) * bind / atan(-power * bind * 10.0);
else uv = p; // no effect for power = 1.0
vec3 col = texture2D(u_Texture, vec2(uv.x, -uv.y * prop)).xyz; // Second part of cheat
gl_FragColor = vec4(col, 1.0);
}
Here's my original shader to display an image that works perfectly :
precision mediump float;
uniform vec4 v_Color;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
void main() {
// premultiplied alpha
vec4 texColor = texture2D(u_Texture, v_TexCoordinate);
// Scale the texture RGB by the vertex color
texColor.rgb *= v_Color.rgb;
// Scale the texture RGBA by the vertex alpha to reinstate premultiplication
gl_FragColor = texColor * v_Color.a;
}
Here's the link to the expected result on ShaderToy :
ShaderToy fisheye
Original result image :
With my shader :
With Rabbid76 solution :
With power = 1.1 :
With solution n2 and power = 10 (bigger image to see better) :
There's some background behind the text, don't pay attention to it ;)
In your shader code fragCoord is assumed to be a window coordinate, were the minimum is (0, 0) and the maximum is the width and height of the viewport. But in your code v_TexCoordinate is assigned to fragCoord. v_TexCoordinate is the texture corodiante in range [0, 1].
Use gl_FragCoord instead of v_TexCoordinate:
// vec2 fragCoord = v_TexCoordinate; <--- delete
vec2 fragCoord = gl_FragCoord.xy;
vec2 p = fragCoord.xy / iResolution.x;
Or skip dividing by the window resolution:
vec2 fragCoord = v_TexCoordinate;
// vec2 p = fragCoord.xy / iResolution.x; <-- delete
vec2 p = fragCoord.xy * vec2(1.0, iResolution.y/iResolution.x);
If the aspect ratio correction is not needed, then it can be even done:
vec2 p = v_TexCoordinate.xy;
See the WebGL example, where I use your original shader code and applied the suggested changes:
(function loadscene() {
var gl, canvas, prog, bufObj = {};
var texture;
function render(deltaMS) {
texture.bound = texture.bound || texture.bind( 0 );
gl.viewport( 0, 0, vp_size[0], vp_size[1] );
gl.enable( gl.DEPTH_TEST );
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
ShProg.Use( progDraw );
ShProg.SetF2( progDraw, "resolution", vp_size );
ShProg.SetI1( progDraw, "u_texture", 0 );
VertexBuffer.Draw( bufRect );
requestAnimationFrame(render);
}
function initScene() {
canvas = document.getElementById( "texture-canvas");
gl = canvas.getContext( "experimental-webgl" );
//gl = canvas.getContext( "webgl2" );
if ( !gl )
return;
progDraw = ShProg.Create(
[ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
{ source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
] );
progDraw.inPos = gl.getAttribLocation( progDraw.progObj, "inPos" );
if ( progDraw.progObj == 0 )
return;
bufRect = VertexBuffer.Create(
[ { data : [ -1, -1, 1, -1, 1, 1, -1, 1 ], attrSize : 2, attrLoc : progDraw.inPos } ],
[ 0, 1, 2, 0, 2, 3 ] );
texture = new Texture( "https://raw.githubusercontent.com/Rabbid76/graphics-snippets/master/resource/texture/supermario.jpg" );
texture.bound = false;
window.onresize = resize;
resize();
requestAnimationFrame(render);
}
function resize() {
//vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
vp_size = [window.innerWidth, window.innerHeight]
vp_size[0] = vp_size[1] = Math.min(vp_size[0], vp_size[1]);
//vp_size = [256, 256]
canvas.width = vp_size[0];
canvas.height = vp_size[1];
}
var ShProg = {
Create: function (shaderList) {
var shaderObjs = [];
for (var i_sh = 0; i_sh < shaderList.length; ++i_sh) {
var shderObj = this.Compile(shaderList[i_sh].source, shaderList[i_sh].stage);
if (shderObj) shaderObjs.push(shderObj);
}
var prog = {}
prog.progObj = this.Link(shaderObjs)
if (prog.progObj) {
prog.attrInx = {};
var noOfAttributes = gl.getProgramParameter(prog.progObj, gl.ACTIVE_ATTRIBUTES);
for (var i_n = 0; i_n < noOfAttributes; ++i_n) {
var name = gl.getActiveAttrib(prog.progObj, i_n).name;
prog.attrInx[name] = gl.getAttribLocation(prog.progObj, name);
}
prog.uniLoc = {};
var noOfUniforms = gl.getProgramParameter(prog.progObj, gl.ACTIVE_UNIFORMS);
for (var i_n = 0; i_n < noOfUniforms; ++i_n) {
var name = gl.getActiveUniform(prog.progObj, i_n).name;
prog.uniLoc[name] = gl.getUniformLocation(prog.progObj, name);
}
}
return prog;
},
AttrI: function (prog, name) { return prog.attrInx[name]; },
UniformL: function (prog, name) { return prog.uniLoc[name]; },
Use: function (prog) { gl.useProgram(prog.progObj); },
SetI1: function (prog, name, val) { if (prog.uniLoc[name]) gl.uniform1i(prog.uniLoc[name], val); },
SetF1: function (prog, name, val) { if (prog.uniLoc[name]) gl.uniform1f(prog.uniLoc[name], val); },
SetF2: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform2fv(prog.uniLoc[name], arr); },
SetF3: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform3fv(prog.uniLoc[name], arr); },
SetF4: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform4fv(prog.uniLoc[name], arr); },
SetM33: function (prog, name, mat) { if (prog.uniLoc[name]) gl.uniformMatrix3fv(prog.uniLoc[name], false, mat); },
SetM44: function (prog, name, mat) { if (prog.uniLoc[name]) gl.uniformMatrix4fv(prog.uniLoc[name], false, mat); },
Compile: function (source, shaderStage) {
var shaderScript = document.getElementById(source);
if (shaderScript)
source = shaderScript.text;
var shaderObj = gl.createShader(shaderStage);
gl.shaderSource(shaderObj, source);
gl.compileShader(shaderObj);
var status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
if (!status) alert(gl.getShaderInfoLog(shaderObj));
return status ? shaderObj : null;
},
Link: function (shaderObjs) {
var prog = gl.createProgram();
for (var i_sh = 0; i_sh < shaderObjs.length; ++i_sh)
gl.attachShader(prog, shaderObjs[i_sh]);
gl.linkProgram(prog);
status = gl.getProgramParameter(prog, gl.LINK_STATUS);
if ( !status ) alert(gl.getProgramInfoLog(prog));
return status ? prog : null;
} };
var VertexBuffer = {
Create: function(attribs, indices, type) {
var buffer = { buf: [], attr: [], inx: gl.createBuffer(), inxLen: indices.length, primitive_type: type ? type : gl.TRIANGLES };
for (var i=0; i<attribs.length; ++i) {
buffer.buf.push(gl.createBuffer());
buffer.attr.push({ size : attribs[i].attrSize, loc : attribs[i].attrLoc, no_of: attribs[i].data.length/attribs[i].attrSize });
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buf[i]);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array( attribs[i].data ), gl.STATIC_DRAW);
}
gl.bindBuffer(gl.ARRAY_BUFFER, null);
if ( buffer.inxLen > 0 ) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.inx);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
}
return buffer;
},
Draw: function(bufObj) {
for (var i=0; i<bufObj.buf.length; ++i) {
gl.bindBuffer(gl.ARRAY_BUFFER, bufObj.buf[i]);
gl.vertexAttribPointer(bufObj.attr[i].loc, bufObj.attr[i].size, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray( bufObj.attr[i].loc);
}
if ( bufObj.inxLen > 0 ) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufObj.inx);
gl.drawElements(bufObj.primitive_type, bufObj.inxLen, gl.UNSIGNED_SHORT, 0);
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
}
else
gl.drawArrays(bufObj.primitive_type, 0, bufObj.attr[0].no_of );
for (var i=0; i<bufObj.buf.length; ++i)
gl.disableVertexAttribArray(bufObj.attr[i].loc);
gl.bindBuffer( gl.ARRAY_BUFFER, null );
} };
class Texture {
constructor( name, dflt ) {
let texture = this;
this.dflt = dflt || [128,128,128,255]
let image = { "cx": this.dflt.w || 1, "cy": this.dflt.h || 1, "plane": this.dflt.p || this.dflt };
this.size = [image.cx, image.cy];
this.dummyObj = Texture.createTexture2D( image, true )
this.image = new Image(64,64);
this.image.setAttribute('crossorigin', 'anonymous');
this.image.onload = function () {
let cx = 1 << 31 - Math.clz32(texture.image.naturalWidth);
if ( cx < texture.image.naturalWidth ) cx *= 2;
let cy = 1 << 31 - Math.clz32(texture.image.naturalHeight);
if ( cy < texture.image.naturalHeight ) cy *= 2;
var canvas = document.createElement( 'canvas' );
canvas.width = cx;
canvas.height = cy;
var context = canvas.getContext( '2d' );
context.drawImage( texture.image, 0, 0, canvas.width, canvas.height );
texture.textureObj = Texture.createTexture2D( canvas, true );
texture.size = [cx, cy];
}
this.image.src = name;
}
static createTexture2D( image, flipY ) {
let t = gl.createTexture();
gl.activeTexture( gl.TEXTURE0 );
gl.bindTexture( gl.TEXTURE_2D, t );
gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, flipY != undefined && flipY == true );
if ( image.cx && image.cy && image.plane )
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, image.cx, image.cy, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(image.plane) );
else
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 );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT );
gl.bindTexture( gl.TEXTURE_2D, null );
return t;
}
bind( texUnit = 0 ) {
gl.activeTexture( gl.TEXTURE0 + texUnit );
if ( this.textureObj ) {
gl.bindTexture( gl.TEXTURE_2D, this.textureObj );
return true;
}
gl.bindTexture( gl.TEXTURE_2D, this.dummyObj );
return false;
}
};
initScene();
})();
<script id="draw-shader-vs" type="x-shader/x-vertex">
precision mediump float;
attribute vec2 inPos;
void main()
{
gl_Position = vec4( inPos.xy, 0.0, 1.0 );
}
</script>
<script id="draw-shader-fs" type="x-shader/x-fragment">
precision mediump float;
uniform vec2 resolution;
uniform sampler2D u_Texture;
#define RESOLUTION_WIDTH 375.0
#define RESOLUTION_HEIGHT 211.0
#define POWER 2.0
void main( void )
{
vec2 fragCoord = gl_FragCoord.xy;
vec2 iResolution = resolution;
//vec2 fragCoord = v_TexCoordinate;
//vec2 iResolution = vec2(RESOLUTION_WIDTH, RESOLUTION_HEIGHT);
vec2 p = fragCoord.xy / iResolution.x; // normalized coords with some cheat
float prop = iResolution.x / iResolution.y;
vec2 m = vec2(0.5, 0.5 / prop); // center coords
vec2 d = p - m; // vector from center to current fragment
float r = sqrt(dot(d, d)); // distance of pixel from center
float power = POWER;
float bind; // radius of 1:1 effect
if (power > 0.0)
bind = sqrt(dot(m, m)); // stick to corners
else {
if (prop < 1.0)
bind = m.x;
else
bind = m.y;
} // stick to borders
// Weird formulas
vec2 uv;
if (power > 0.0) // fisheye
uv = m + normalize(d) * tan(r * power) * bind / tan( bind * power);
else if (power < 0.0) // antifisheye
uv = m + normalize(d) * atan(r * -power * 10.0) * bind / atan(-power * bind * 10.0);
else uv = p; // no effect for power = 1.0
vec3 col = texture2D(u_Texture, vec2(uv.x, -uv.y * prop)).xyz; // Second part of cheat
gl_FragColor = vec4(col, 1.0);
}
</script>
<body>
<canvas id="texture-canvas" style="border: none"></canvas>
</body>

Randomly Generating Curved/Wavy Paths

I have a massive image of a map that is much larger than the viewport and centered in the viewport, which can be explored by the user by dragging the screen. In order to create a parallax effect, I used a massive image of clouds in the foreground. As the user explores the map via dragging, both the background and foreground move in a parallax fashion. So far, so good.
However, what I really want to do is give the image of clouds a "default" movement that would be randomly generated on each page load, so that the clouds would always be moving, even if the user is not dragging. I know this can be done by animating the foreground along a path, but I am not exactly sure how to go about this.
How can I randomly generate irregularly curved or wavy paths on each page load?
Does anybody know of any algorithms that can do this?
I also use a copy of the previous answers to realize a simplified version of what I hinted at in the comments.
Use a random walk on the unit circle, that is on the angle, to determine a velocity vector that slowly but randomly changes and move forward using cubic Bezier patches.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 400;
var cx = cw / 4, cy = ch / 2;
var angVel = v.value;
var tension = t.value;
ctx.lineWidth = 4;
var npts = 60;
var dw = Array();
var xs = Array();
var ys = Array();
var vxs = Array();
var vys = Array();
function Randomize() {
for (var i = 0; i < npts; i++) {
dw[i] = (2*Math.random()-1);
}
}
function ComputePath() {
xs[0]=cx; ys[0]=cy;
var angle = 0;
for (var i = 0; i < npts; i++) {
vxs[i]=10*Math.cos(2*Math.PI*angle);
vys[i]=10*Math.sin(2*Math.PI*angle);
angle = angle + dw[i]*angVel;
}
for (var i = 1; i < npts; i++) {
xs[i] = xs[i-1]+3*(vxs[i-1]+vxs[i])/2;
ys[i] = ys[i-1]+3*(vys[i-1]+vys[i])/2;
}
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
ctx.moveTo(xs[0],ys[0]);
for (var i = 1; i < npts; i++) {
var cp1x = xs[i-1]+tension*vxs[i-1];
var cp1y = ys[i-1]+tension*vys[i-1];
var cp2x = xs[i]-tension*vxs[i];
var cp2y = ys[i]-tension*vys[i]
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, xs[i], ys[i]);
}
ctx.stroke();
}
Randomize();
ComputePath();
Draw();
r.addEventListener("click",()=>{
Randomize();
ComputePath();
Draw();
})
v.addEventListener("input",()=>{
angVel = v.value;
vlabel.innerHTML = ""+angVel;
ComputePath();
Draw();
})
t.addEventListener("input",()=>{
tension = t.value;
tlabel.innerHTML = ""+tension;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<table>
<tr><td>angular velocity:</td><td> <input type="range" id="v" min ="0" max = "0.5" step = "0.01" value="0.2" /></td><td id="vlabel"></td></tr>
<tr><td>tension</td><td> <input type="range" id="t" min ="0" max = "1" step = "0.1" value="0.8" /></td><td id="tlabel"></td></tr>
<tr><td>remix</td><td> <button id="r"> + </button></td><td></td></tr>
</table>
If your question is: How can I randomly generate curved or wavy paths? this is how I would do it: I'm using inputs type range to change the value for amplitude and frequency, but you can set those values randomly on load.
I hope it helps.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 800;
var ch = c.height = 150;
var cx = cw / 2,
cy = ch / 2;
var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (var x = 0; x < cw; x++) {
y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
a.addEventListener("input",()=>{
amplitude = a.value;
Draw();
})
f.addEventListener("input",()=>{
frequency = f.value;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>frequency: <input type="range" id="f" min ="0.01" max = "0.1" step = "0.001" value=".05" /></p>
<p>amplitude: <input type="range" id="a" min ="1" max = "100" value="50" /></p>
I was impressed by the functionality to be able to draw canvases in the SO answers, so I "stole" enxaneta code snippet and played a bit with it (hope that is ok).
The idea is to generate several random points (xs, ys) and for each x from the path to interpolate the y as y = sum{ys_i*w_i}/sum{w_i}, where w_i is some interpolation weight as a function of x. For example w_i(x) = (xs_i - x)^(-2). Hope this makes sense - if this is of any interested I'll try to provide more details.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 150;
var cx = cw / 2,
cy = ch / 2;
var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;
var npts = 20;
var xs = Array();
var ys = Array();
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i;
ys[i] = 2.0*(Math.random()-0.5)*amplitude;
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (var x = 0; x < cw; x++) {
y = 0.0;
wsum = 0.0;
for (var i = -5; i <= 5; i++) {
xx = x;
ii = Math.round(x/xs[1]) + i;
if (ii < 0) { xx += cw; ii += npts; }
if (ii >= npts) { xx -= cw; ii -= npts; }
w = Math.abs(xs[ii] - xx);
w = Math.pow(w, frequency);
y += w*ys[ii];
wsum += w;
}
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
a.addEventListener("input",()=>{
amplitude = a.value;
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i;
ys[i] = 2.0*(Math.random()-0.5)*amplitude;
}
Draw();
})
f.addEventListener("input",()=>{
frequency = f.value;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>amplitude: <input type="range" id="a" min ="1" max = "100" value="50" /></p>
<p>frequency: <input type="range" id="f" min ="-10" max = "1" step = "0.1" value="-2" hidden/></p>
Deterministic random paths
Storing paths for random movements is not needed. Also random is another way of being very complex, and for humans it does not take much complexity to look randoms.
Thus with a little randomness to add to complexity you can make the appearance of the infinite non repeating sequence that and be rewound, stopped, slowed down speed up, and be fully deterministic and requiring only a single value to store.
Complex cycles.
To move a point in a circle around a center you can use sin and cos.
For example a point x,y and you want to move in a ball around that point at a distance of dist and a rate once a second. Example in snippet.
var px = 100; // point of rotation.
var py = 100;
const RPS = 1; // Rotations Per Second
const dist = 50; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
time = (time / 1000) * PI2 * RPS; // convert the time to rotations per secon
const xx = Math.cos(time) * dist;
const yy = Math.sin(time) * dist;
drawCircle(xx, yy)
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
Next let's move the point around which we rotate, using the method above.
Then for the ball we can change the phase of the rotation in x from the rotation in y. This means that the ball rotating around the now rotating point, and the balls rotating axis are out of phase.
The result is a more complex movements.
var px = 100; // point of rotation.
var py = 100;
const RPS_P = 0.1; // point Rotations Per Second 0.1 every 10 seconds
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_P = 30; // distance from center point is
const dist = 50; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
var phaseX = (time / 1000) * PI2 * RPS_X;
var phaseY = (time / 1000) * PI2 * RPS_Y;
const xx = Math.cos(phaseX) * dist;
const yy = Math.sin(phaseY) * dist;
drawCircle(xx, yy)
}
function movePoint(time) { // move point around center
time = (time / 1000) * PI2 * RPS_P;
px = 100 + Math.cos(time) * dist_P;
py = 100 + Math.sin(time) * dist_P;
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
movePoint(time);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
We can continue to add out of phase rotations. In the next example we now rotate the rotation point around the center, add out of phase rotation to that point and finally draw the ball with its out of phase rotation.
var px = 100; // point of rotation.
var py = 100;
const RPS_C_X = 0.43; // Rotation speed X of rotating rotation point
const RPS_C_Y = 0.47; // Rotation speed X of rotating rotation point
const RPS_P_X = 0.093; // point Rotations speed X
const RPS_P_Y = 0.097; // point Rotations speed Y
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_C = 20; // distance from center point is
const dist_P = 30; // distance from center point is
const dist = 30; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
var phaseX = (time / 1000) * PI2 * RPS_X;
var phaseY = (time / 1000) * PI2 * RPS_Y;
const xx = Math.cos(phaseX) * dist;
const yy = Math.sin(phaseY) * dist;
drawCircle(xx, yy)
}
function movePoints(time) { // Move the rotating pointe and rotate the rotation point
// around that point
var phaseX = (time / 1000) * PI2 * RPS_C_X;
var phaseY = (time / 1000) * PI2 * RPS_C_Y;
px = 100 + Math.cos(phaseX) * dist_C;
py = 100 + Math.sin(phaseY) * dist_C;
phaseX = (time / 1000) * PI2 * RPS_P_X;
phaseY = (time / 1000) * PI2 * RPS_P_Y;
px = px + Math.cos(phaseX) * dist_P;
py = py + Math.sin(phaseY) * dist_P;
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
movePoints(time);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
So now we have a very complex rotation. However as it is set to the time, you can repeat the movement by just setting the time back to the start. You don't need to store a long complex path.
Add a little random
You may see some repeating movement but if you make the phases of each axis a prime then the repeat time is the product of all the primes.
If you want many objects each with a different movement you can randomise the rotation rates and many more properties.
Javascript does not have a seeded random generator. However you can create one. With a seeded random generator you can us the seed to generate a random object. But if you use that seed again you get the same object. In the example below I us a seed from 0 to 10000000 to create a cloud. That means there are 10000000 unique clouds, but all repeatable.
Example of deterministic random clouds
Restart and it will repeat exactly the same. To change it to non deterministic random just add randSeed(Math.random() * 100000 | 0)
const seededRandom = (() => {
var seed = 1;
return { max : 2576436549074795, reseed (s) { seed = s }, random () { return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max }}
})();
const randSeed = (seed) => seededRandom.reseed(seed|0);
const randSI = (min = 2, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const randSPow = (min, max = min + (min = 0), p = 2) => (max + min) / 2 + (Math.pow(seededRandom.random() / seededRandom.max, p) * (max - min) * 0.5) * (randSI(2) < 1 ? 1 : -1);
const ctx = canvas.getContext("2d");
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const DIAG = (W * W + H * H) ** 0.5;
const colors = {
dark : {
minRGB : [100 * 0.6,200 * 0.6,240 * 0.6],
maxRGB : [255 * 0.6,255 * 0.6,255 * 0.6],
},
light : {
minRGB : [100,200,240],
maxRGB : [255,255,255],
},
}
const getCol = (pos, range) => "rgba(" +
((range.maxRGB[0] - range.minRGB[0]) * pos + range.minRGB[0] | 0) + "," +
((range.maxRGB[1] - range.minRGB[1]) * pos + range.minRGB[1] | 0) + "," +
((range.maxRGB[2] - range.minRGB[2]) * pos + range.minRGB[2] | 0) + "," +(pos * 0.2 + 0.8) + ")";
const Cloud = {
x : 0,
y : 0,
dir : 0, // in radians
wobble : 0,
wobble1 : 0,
wSpeed : 0,
wSpeed1 : 0,
mx : 0, // Move offsets
my : 0,
seed : 0,
size : 2,
detail : null,
reset : true, // when true could resets
init() {
this.seed = randSI(10000000);
this.reset = false;
var x,y,r,dir,dist,f;
if (this.detail === null) { this.detail = [] }
else { this.detail.length = 0 }
randSeed(this.seed);
this.size = randSPow(2, 8); // The pow add bias to smaller values
var col = (this.size -2) / 6;
this.col1 = getCol(col,colors.dark)
this.col2 = getCol(col,colors.light)
var flufCount = randSI(5,15);
while (flufCount--) {
x = randSI(-this.size * 8, this.size * 8);
r = randS(this.size * 2, this.size * 8);
dir = randS(Math.PI * 2);
dist = randSPow(1) * r ;
this.detail.push(f = {x,r,y : 0,mx:0,my:0, move : randS(0.001,0.01), phase : randS(Math.PI * 2)});
f.x+= Math.cos(dir) * dist;
f.y+= Math.sin(dir) * dist;
}
this.xMax = this.size * 12 + this.size * 10 + this.size * 4;
this.yMax = this.size * 10 + this.size * 4;
this.wobble = randS(Math.PI * 2);
this.wSpeed = randS(0.01,0.02);
this.wSpeed1 = randS(0.01,0.02);
const aOff = randS(1) * Math.PI * 0.5 - Math.PI *0.25;
this.x = W / 2 - Math.cos(this.dir+aOff) * DIAG * 0.7;
this.y = H / 2 - Math.sin(this.dir+aOff) * DIAG * 0.7;
clouds.sortMe = true; // flag that coulds need resort
},
move() {
var dx,dy;
this.dir = gTime / 10000;
if(this.reset) { this.init() }
this.wobble += this.wSpeed;
this.wobble1 += this.wSpeed1;
this.mx = Math.cos(this.wobble) * this.size * 4;
this.my = Math.sin(this.wobble1) * this.size * 4;
this.x += dx = Math.cos(this.dir) * this.size / 5;
this.y += dy = Math.sin(this.dir) * this.size / 5;
if (dx > 0 && this.x > W + this.xMax ) { this.reset = true }
else if (dx < 0 && this.x < - this.xMax ) { this.reset = true }
if (dy > 0 && this.y > H + this.yMax) { this.reset = true }
else if (dy < 0 && this.y < - this.yMax) { this.reset = true }
},
draw(){
const s = this.size;
const s8 = this.size * 8;
ctx.fillStyle = this.col1;
ctx.setTransform(1,0,0,1,this.x+ this.mx,this.y +this.my);
ctx.beginPath();
for (const fluf of this.detail) {
fluf.phase += fluf.move + Math.sin(this.wobble * this.wSpeed1) * 0.02 * Math.cos(fluf.phase);
fluf.mx = Math.cos(fluf.phase) * fluf.r / 2;
fluf.my = Math.sin(fluf.phase) * fluf.r / 2;
const x = fluf.x + fluf.mx;
const y = fluf.y + fluf.my;
ctx.moveTo(x + fluf.r + s, y);
ctx.arc(x,y,fluf.r+ s,0,Math.PI * 2);
}
ctx.fill();
ctx.fillStyle = this.col2;
ctx.globalAlpha = 0.5;
ctx.beginPath();
for (const fluf of this.detail) {
const x = fluf.x + fluf.mx - s;
const y = fluf.y + fluf.my - s;
ctx.moveTo(x + fluf.r, y);
ctx.arc(x,y,fluf.r,0,Math.PI * 2);
}
ctx.fill();
ctx.globalAlpha = 0.6;
ctx.beginPath();
for (const fluf of this.detail) {
const x = fluf.x + fluf.mx - s * 1.4;
const y = fluf.y + fluf.my - s * 1.4;
ctx.moveTo(x + fluf.r * 0.8, y);
ctx.arc(x,y,fluf.r* 0.8,0,Math.PI * 2);
}
ctx.fill();
ctx.globalAlpha = 1;
}
}
function createCloud(size){ return {...Cloud} }
const clouds = Object.assign([],{
move() { for(const cloud of this){ cloud.move() } },
draw() { for(const cloud of this){ cloud.draw() } },
sortMe : true, // if true then needs to resort
resort() {
this.sortMe = false;
this.sort((a,b)=>a.size - b.size);
}
});
for(let i = 0; i < 15; i ++) { clouds.push(createCloud(40)) }
requestAnimationFrame(mainLoop)
var gTime = 0;
function mainLoop() {
gTime += 16;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
if(clouds.sortMe) { clouds.resort() }
clouds.move();
clouds.draw();
requestAnimationFrame(mainLoop);
}
body { padding : 0px; margin : 0px;}
canvas {
background : rgb(60,120,148);
border : 1px solid black;
}
<canvas id="canvas" width="600" height="200"></canvas>

Canvas/WebGL 2D tilemap grid artifacts

I am creating a simple 2D web game that works with your typical tile map and sprites.
The twist is that I want smooth camera controls, both translation and scaling (zooming).
I tried using both the Canvas 2D API, and WebGL, and in both I simply cannot avoid the bleeding grid line artifacts, while also supporting zooming properly.
If it matters, all of my tiles are of size 1, and scaled to whatever size is needed, all of their coordinates are integers, and I am using a texture atlas.
Here's an example picture using my WebGL code, where the thin red/white lines are not wanted.
I remember writing sprite tile maps years ago with desktop GL, ironically using similar code (more or less equivalent to what I could do with WebGL 2), and it never had any of these issues.
I am considering to try DOM based elements next, but I fear it will not feel or look smooth.
One solution is to draw the tiles in the fragment shader
So you have your map, say a Uint32Array. Break it down into units of 4 bytes each. First 2 bytes are the tile ID, last byte is flags
As you walk across the quad for each pixel you lookup in the tilemap texture which tile it is, then you use that to compute UV coordinates to get pixels from that tile out of the texture of tiles. If your texture of tiles has gl.NEAREST sampling set then you'll never get any bleeding
Note that unlike traditional tilemaps the ids of each tile is the X,Y coordinate of the tile in the tile texture. In other words if your tile texture has 16x8 tiles across and you want your map to show the tile 7 over and 4 down then the id of that tile is 7,4 (first byte 7, second byte 4) where as in a traditional CPU based system the tile id would probably be 4*16+7 or 71 (the 71st tile). You could add code to the shader to do more traditional indexing but since the shader has to convert the id into 2d texture coords it just seemed easier to use 2d ids.
const vs = `
attribute vec4 position;
//attribute vec4 texcoord; - since position is a unit square just use it for texcoords
uniform mat4 u_matrix;
uniform mat4 u_texMatrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
// v_texcoord = (u_texMatrix * texccord).xy;
v_texcoord = (u_texMatrix * position).xy;
}
`;
const fs = `
precision highp float;
uniform sampler2D u_tilemap;
uniform sampler2D u_tiles;
uniform vec2 u_tilemapSize;
uniform vec2 u_tilesetSize;
varying vec2 v_texcoord;
void main() {
vec2 tilemapCoord = floor(v_texcoord);
vec2 texcoord = fract(v_texcoord);
vec2 tileFoo = fract((tilemapCoord + vec2(0.5, 0.5)) / u_tilemapSize);
vec4 tile = floor(texture2D(u_tilemap, tileFoo) * 256.0);
float flags = tile.w;
float xflip = step(128.0, flags);
flags = flags - xflip * 128.0;
float yflip = step(64.0, flags);
flags = flags - yflip * 64.0;
float xySwap = step(32.0, flags);
if (xflip > 0.0) {
texcoord = vec2(1.0 - texcoord.x, texcoord.y);
}
if (yflip > 0.0) {
texcoord = vec2(texcoord.x, 1.0 - texcoord.y);
}
if (xySwap > 0.0) {
texcoord = texcoord.yx;
}
vec2 tileCoord = (tile.xy + texcoord) / u_tilesetSize;
vec4 color = texture2D(u_tiles, tileCoord);
if (color.a <= 0.1) {
discard;
}
gl_FragColor = color;
}
`;
const tileWidth = 32;
const tileHeight = 32;
const tilesAcross = 8;
const tilesDown = 4;
const m4 = twgl.m4;
const gl = document.querySelector('#c').getContext('webgl');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// gl.createBuffer, bindBuffer, bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
});
function r(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + (max - min) * Math.random();
}
// make some tiles
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = tileWidth * tilesAcross;
ctx.canvas.height = tileHeight * tilesDown;
ctx.font = "bold 24px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const f = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~';
for (let y = 0; y < tilesDown; ++y) {
for (let x = 0; x < tilesAcross; ++x) {
const color = `hsl(${r(360) | 0},${r(50,100)}%,50%)`;
ctx.fillStyle = color;
const tx = x * tileWidth;
const ty = y * tileHeight;
ctx.fillRect(tx, ty, tileWidth, tileHeight);
ctx.fillStyle = "#FFF";
ctx.fillText(f.substr(y * 8 + x, 1), tx + tileWidth * .5, ty + tileHeight * .5);
}
}
document.body.appendChild(ctx.canvas);
const tileTexture = twgl.createTexture(gl, {
src: ctx.canvas,
minMag: gl.NEAREST,
});
// make a tilemap
const mapWidth = 400;
const mapHeight = 300;
const tilemap = new Uint32Array(mapWidth * mapHeight);
const tilemapU8 = new Uint8Array(tilemap.buffer);
const totalTiles = tilesAcross * tilesDown;
for (let i = 0; i < tilemap.length; ++i) {
const off = i * 4;
// mostly tile 9
const tileId = r(10) < 1
? (r(totalTiles) | 0)
: 9;
tilemapU8[off + 0] = tileId % tilesAcross;
tilemapU8[off + 1] = tileId / tilesAcross | 0;
const xFlip = r(2) | 0;
const yFlip = r(2) | 0;
const xySwap = r(2) | 0;
tilemapU8[off + 3] =
(xFlip ? 128 : 0) |
(yFlip ? 64 : 0) |
(xySwap ? 32 : 0) ;
}
const mapTexture = twgl.createTexture(gl, {
src: tilemapU8,
width: mapWidth,
minMag: gl.NEAREST,
});
function ease(t) {
return Math.cos(t) * .5 + .5;
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeLerp(a, b, t) {
return lerp(a, b, ease(t));
}
function render(time) {
time *= 0.001; // convert to seconds;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const mat = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
m4.scale(mat, [gl.canvas.width, gl.canvas.height, 1], mat);
const scaleX = easeLerp(.5, 2, time * 1.1);
const scaleY = easeLerp(.5, 2, time * 1.1);
const dispScaleX = 1;
const dispScaleY = 1;
// origin of scale/rotation
const originX = gl.canvas.width * .5;
const originY = gl.canvas.height * .5;
// scroll position in pixels
const scrollX = time % (mapWidth * tileWidth );
const scrollY = time % (mapHeight * tileHeight);
const rotation = time;
const tmat = m4.identity();
m4.translate(tmat, [scrollX, scrollY, 0], tmat);
m4.rotateZ(tmat, rotation, tmat);
m4.scale(tmat, [
gl.canvas.width / tileWidth / scaleX * (dispScaleX),
gl.canvas.height / tileHeight / scaleY * (dispScaleY),
1,
], tmat);
m4.translate(tmat, [
-originX / gl.canvas.width,
-originY / gl.canvas.height,
0,
], tmat);
twgl.setUniforms(programInfo, {
u_matrix: mat,
u_texMatrix: tmat,
u_tilemap: mapTexture,
u_tiles: tileTexture,
u_tilemapSize: [mapWidth, mapHeight],
u_tilesetSize: [tilesAcross, tilesDown],
});
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<canvas id="c"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

Dart WebGL not rendering

I'm trying to write something with WebGL and Dart but I'm having problems. This is my source code.
This is my main file:
library warmup;
import 'dart:html';
import 'dart:math';
import 'dart:web_gl' as WebGL;
import 'dart:typed_data';
import 'package:vector_math/vector_math.dart';
part 'graphics.dart';
WebGL.RenderingContext gl;
CanvasElement canvas;
QuadRenderer renderer;
Random random = new Random();
void main() {
querySelector("#paragraf").setInnerHtml("HELLO!");
canvas = querySelector("#game_canvas");
new Game().start();
}
class Game {
Texture test = new Texture("tex/test.png");
void start() {
gl = canvas.getContext3d();
if (gl == null) {
print("No WebGL!");
}
Texture.loadAll();
renderer = new QuadRenderer();
renderer.projMatrix = makeOrthographicMatrix(0, canvas.width, 0, canvas.height, -1, 1);
gl.disable(WebGL.DEPTH_TEST);
gl.enable(WebGL.BLEND);
gl.blendFunc(WebGL.SRC_ALPHA, WebGL.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
window.requestAnimationFrame(_update);
}
int now, last = new DateTime.now().millisecondsSinceEpoch;
double unprocessedFrames;
void _update(double time) {
now = new DateTime.now().millisecondsSinceEpoch;
unprocessedFrames = (now - last) * 60 / 1000;
while (unprocessedFrames > 1.0) {
tick();
unprocessedFrames--;
}
renderGame();
window.requestAnimationFrame(_update);
}
void tick() {
}
void renderGame() {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clear(WebGL.COLOR_BUFFER_BIT);
renderer.bindTexture(test);
renderer.renderQuad(new Vector2(canvas.width / 2.0, canvas.height / 2.0), 128, 128, 0, 0, false);
}
}
This is my graphics.dart file:
part of warmup;
class Texture {
static List<Texture> _pending = new List<Texture>();
String url;
int width, height;
WebGL.Texture data;
bool loaded = false;
Texture(this.url) {
if (gl == null) {
_pending.add(this);
}
else {
load();
}
}
static void loadAll() {
_pending.forEach((e) => e.load());
_pending.clear();
}
void load() {
ImageElement img = new ImageElement();
data = gl.createTexture();
img.onLoad.listen((e) {
gl.bindTexture(WebGL.TEXTURE_2D, data);
gl.texImage2DImage(WebGL.TEXTURE_2D, 0, WebGL.RGBA, WebGL.RGBA, WebGL.UNSIGNED_BYTE, img);
gl.texParameteri(WebGL.TEXTURE_2D, WebGL.TEXTURE_MIN_FILTER, WebGL.NEAREST);
gl.texParameteri(WebGL.TEXTURE_2D, WebGL.TEXTURE_MAG_FILTER, WebGL.NEAREST);
width = img.width;
height = img.height;
loaded = true;
});
img.src = url;
}
}
class Vertex {
Vector2 pos;
Vector4 color;
Vector2 texCoord;
static int elementBytes = 8;
static int posElementCount = 2;
static int colorElementCount = 4;
static int textureElementCount = 2;
static int posBytesCount = posElementCount * elementBytes;
static int colorByteCount = colorElementCount * elementBytes;
static int textureByteCount = textureElementCount * elementBytes;
static int posByteOffset = 0;
static int colorByteOffset = posByteOffset + posBytesCount;
static int textureByteOffset = colorByteOffset + colorByteCount;
static int elementCount = posElementCount +
colorElementCount + textureElementCount;
static int stride = posBytesCount + colorByteCount +
textureByteCount;
Vertex() {
pos = new Vector2(0.0, 0.0);
color = new Vector4(1.0, 1.0, 1.0, 1.0);
texCoord = new Vector2(0.0, 0.0);
}
List<double> getElements() {
List<double> result;
result = [pos.x, pos.y, color.r, color.g, color.b, color.a, texCoord.x, texCoord.y];
return result;
}
}
class QuadRenderer {
String _vsSource = """
precision highp float;
attribute vec2 a_pos;
attribute vec2 a_texCoord;
attribute vec4 a_color;
uniform mat4 proj;
uniform mat4 model;
varying vec2 v_texCoord;
varying vec4 v_pos;
varying vec4 v_color;
void main() {
v_pos = proj * model * vec4(a_pos, 0, 1);
v_color = a_color;
gl_Position = proj * model * vec4(a_pos, 0, 1);
}
""", _fsSource = """
precision highp float;
uniform sampler2D texture;
varying vec2 v_texCoord;
varying vec4 v_pos;
varying vec4 v_color;
void main() {
vec4 texColor = texture2D(texture, v_texCoord);
gl_FragColor = vec4(1, 1, 1, 1);
}
""";
WebGL.Shader vs, fs;
WebGL.Program program;
WebGL.Buffer vab, iab;
int posLocation;
Matrix4 projMatrix;
Vertex v0, v1, v2, v3;
WebGL.UniformLocation projLocation, modelLocation;
Texture texture;
void renderQuad(Vector2 pos, num w, num h, num uo, num vo, bool normalize, {Vector4 color, double rotation}) {
if (!texture.loaded) return;
if (color == null) color = new Vector4(1.0, 1.0, 1.0, 1.0);
if (rotation == null) rotation = 0.0;
Matrix4 model = new Matrix4.identity();
model.translate(pos.x, pos.y);
model.scale(w*1.0, h*1.0, 0.0);
gl.uniformMatrix4fv(modelLocation, false, model.storage);
gl.uniformMatrix4fv(projLocation, false, projMatrix.storage);
if (normalize) {
uo /= texture.width;
vo /= texture.height;
}
v0.color = v1.color = v2.color = v3.color = color;
v0.texCoord = new Vector2(uo + w * 1.0, vo + h * 1.0); v1.texCoord = new Vector2(uo + w * 1.0, vo * 1.0);
v2.texCoord = new Vector2(uo * 1.0, vo * 1.0); v3.texCoord = new Vector2(uo * 1.0, vo + h * 1.0);
_compileVertices();
gl.drawElements(WebGL.TRIANGLES, 6, WebGL.UNSIGNED_SHORT, 0);
}
void bindTexture(Texture tex) {
texture = tex;
gl.bindTexture(WebGL.TEXTURE_2D, texture.data);
}
void _compileVertices() {
List<Vertex> vertices = [v0, v1, v2, v3];
Float32List vertexBuffer = new Float32List(vertices.length * Vertex.elementCount);
for (int i = 0; i < vertices.length; i++) {
Vertex vertex = vertices[i];
vertexBuffer.setAll(i * Vertex.elementCount, vertex.getElements());
}
gl.bindBuffer(WebGL.ARRAY_BUFFER, vab);
gl.bufferData(WebGL.ARRAY_BUFFER, vertexBuffer, WebGL.STATIC_DRAW);
gl.vertexAttribPointer(0, Vertex.posElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.posByteOffset);
gl.vertexAttribPointer(1, Vertex.colorElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.colorByteOffset);
gl.vertexAttribPointer(2, Vertex.textureElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.textureByteOffset);
}
QuadRenderer() {
vs = gl.createShader(WebGL.VERTEX_SHADER);
gl.shaderSource(vs, _vsSource);
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, WebGL.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(vs);
}
fs = gl.createShader(WebGL.FRAGMENT_SHADER);
gl.shaderSource(fs, _fsSource);
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, WebGL.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(fs);
}
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, WebGL.LINK_STATUS)) {
throw gl.getProgramInfoLog(program);
}
projLocation = gl.getUniformLocation(program, "proj");
modelLocation = gl.getUniformLocation(program, "model");
gl.useProgram(program);
v0 = new Vertex(); v1 = new Vertex(); v2 = new Vertex(); v3 = new Vertex();
v0.pos = new Vector2(0.5, 0.5); v1.pos = new Vector2(0.5, -0.5);
v2.pos = new Vector2(-0.5, -0.5); v3.pos = new Vector2(-0.5, 0.5);
gl.bindAttribLocation(program, 0, "a_pos");
gl.bindAttribLocation(program, 1, "a_color");
gl.bindAttribLocation(program, 2, "a_texCoord");
List<Vertex> vertices = [v0, v1, v2, v3];
Float32List vertexBuffer = new Float32List(vertices.length * Vertex.elementCount);
for (int i = 0; i < vertices.length; i++) {
Vertex vertex = vertices[i];
vertexBuffer.setAll(i * Vertex.elementCount, vertex.getElements());
}
vab = gl.createBuffer();
gl.bindBuffer(WebGL.ARRAY_BUFFER, vab);
gl.bufferData(WebGL.ARRAY_BUFFER, vertexBuffer, WebGL.STATIC_DRAW);
gl.vertexAttribPointer(0, Vertex.posElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.posByteOffset);
gl.vertexAttribPointer(1, Vertex.colorElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.colorByteOffset);
gl.vertexAttribPointer(2, Vertex.textureElementCount, WebGL.FLOAT, false, Vertex.stride, Vertex.textureByteOffset);
Int16List indexBuffer = new Int16List(6);
indexBuffer.setAll(0, [0, 1, 2, 0, 2, 3]);
iab = gl.createBuffer();
gl.bindBuffer(WebGL.ELEMENT_ARRAY_BUFFER, iab);
gl.bufferData(WebGL.ELEMENT_ARRAY_BUFFER, indexBuffer, WebGL.STATIC_DRAW);
}
}
The problem was that the render call renderGame() doesn't work. I know that WebGL work, cause the clear color works fine. I do not know what the problem is, so any suggestion is appreciated.
How about using a rendering library or game engine like Pixi Dart or StageXL instead of plain WebGL?
Those two libraries also abstract the renderer backend (use WebGL whenever possible and use canvas as a fallback).
Hope that helps if you're thinking on developing a game :)

Resources