A u_pixelRatio uniform was added to be used in the shaders. this is necessary since we're relying on the builtin gl_FragCoord vector, which will be scaled relative to the u_sizePx uniform in case of a device pixel ratio != 1. Also added tests for computed uniform values, instead of just testing that they were indeed set.
474 lines
14 KiB
JavaScript
474 lines
14 KiB
JavaScript
import WebGLArrayBuffer from '../../../../../src/ol/webgl/Buffer.js';
|
|
import WebGLHelper, {
|
|
DefaultUniform,
|
|
} from '../../../../../src/ol/webgl/Helper.js';
|
|
import {ARRAY_BUFFER, FLOAT, STATIC_DRAW} from '../../../../../src/ol/webgl.js';
|
|
import {
|
|
create as createTransform,
|
|
rotate as rotateTransform,
|
|
scale as scaleTransform,
|
|
translate as translateTransform,
|
|
} from '../../../../../src/ol/transform.js';
|
|
import {getUid} from '../../../../../src/ol/util.js';
|
|
|
|
const VERTEX_SHADER = `
|
|
precision mediump float;
|
|
|
|
uniform mat4 u_offsetScaleMatrix;
|
|
uniform mat4 u_offsetRotateMatrix;
|
|
uniform float u_time;
|
|
uniform float u_zoom;
|
|
uniform float u_resolution;
|
|
|
|
attribute float a_test;
|
|
uniform float u_test;
|
|
|
|
void main(void) {
|
|
gl_Position = vec4(u_test, a_test, 0.0, 1.0);
|
|
}`;
|
|
|
|
const INVALID_VERTEX_SHADER = `
|
|
precision mediump float;
|
|
|
|
uniform mat4 u_offsetScaleMatrix;
|
|
uniform mat4 u_offsetRotateMatrix;
|
|
uniform float u_time;
|
|
uniform float u_zoom;
|
|
uniform float u_resolution;
|
|
|
|
bla
|
|
uniform float u_test;
|
|
|
|
void main(void) {
|
|
gl_Position = vec4(u_test, a_test, 0.0, 1.0);
|
|
}`;
|
|
|
|
const FRAGMENT_SHADER = `
|
|
precision mediump float;
|
|
|
|
void main(void) {
|
|
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
|
}`;
|
|
|
|
const INVALID_FRAGMENT_SHADER = `
|
|
precision mediump float;
|
|
|
|
void main(void) {
|
|
gl_FragColor = vec4(oops, 1.0, 1.0, 1.0);
|
|
}`;
|
|
|
|
const SAMPLE_FRAMESTATE = {
|
|
size: [100, 150],
|
|
viewState: {
|
|
rotation: 0.4,
|
|
resolution: 2,
|
|
center: [10, 20],
|
|
zoom: 3,
|
|
},
|
|
};
|
|
|
|
describe('ol/webgl/WebGLHelper', function () {
|
|
let h;
|
|
afterEach(function () {
|
|
h.dispose();
|
|
});
|
|
|
|
describe('constructor', function () {
|
|
describe('without an argument', function () {
|
|
beforeEach(function () {
|
|
h = new WebGLHelper();
|
|
});
|
|
|
|
it('initialized WebGL context & canvas', function () {
|
|
expect(h.getGL() instanceof WebGLRenderingContext).to.eql(true);
|
|
expect(h.getCanvas() instanceof HTMLCanvasElement).to.eql(true);
|
|
});
|
|
|
|
it('has a default rendering pass', function () {
|
|
expect(h.postProcessPasses_.length).to.eql(1);
|
|
});
|
|
});
|
|
|
|
describe('with post process passes', function () {
|
|
beforeEach(function () {
|
|
h = new WebGLHelper({
|
|
postProcesses: [
|
|
{
|
|
scaleRatio: 0.5,
|
|
},
|
|
{
|
|
uniforms: {
|
|
u_test: 4,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('has instantiated post-processing passes', function () {
|
|
expect(h.postProcessPasses_.length).to.eql(2);
|
|
expect(h.postProcessPasses_[0].scaleRatio_).to.eql(0.5);
|
|
expect(h.postProcessPasses_[0].uniforms_.length).to.eql(0);
|
|
expect(h.postProcessPasses_[1].scaleRatio_).to.eql(1);
|
|
expect(h.postProcessPasses_[1].uniforms_.length).to.eql(1);
|
|
expect(h.postProcessPasses_[1].uniforms_[0].value).to.eql(4);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('operations', function () {
|
|
describe('prepare draw', function () {
|
|
beforeEach(function () {
|
|
h = new WebGLHelper({
|
|
uniforms: {
|
|
u_test1: 42,
|
|
u_test2: [1, 3],
|
|
u_test3: document.createElement('canvas'),
|
|
u_test4: createTransform(),
|
|
},
|
|
});
|
|
h.useProgram(
|
|
h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER),
|
|
SAMPLE_FRAMESTATE
|
|
);
|
|
h.prepareDraw({
|
|
pixelRatio: 2,
|
|
size: [50, 80],
|
|
viewState: {
|
|
rotation: 10,
|
|
resolution: 10,
|
|
center: [0, 0],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('has resized the canvas', function () {
|
|
expect(h.getCanvas().width).to.eql(100);
|
|
expect(h.getCanvas().height).to.eql(160);
|
|
});
|
|
|
|
it('has processed default uniforms', function () {
|
|
expect(
|
|
h.uniformLocations_[DefaultUniform.OFFSET_ROTATION_MATRIX]
|
|
).not.to.eql(undefined);
|
|
expect(
|
|
h.uniformLocations_[DefaultUniform.OFFSET_SCALE_MATRIX]
|
|
).not.to.eql(undefined);
|
|
expect(h.uniformLocations_[DefaultUniform.TIME]).not.to.eql(undefined);
|
|
expect(h.uniformLocations_[DefaultUniform.ZOOM]).not.to.eql(undefined);
|
|
expect(h.uniformLocations_[DefaultUniform.RESOLUTION]).not.to.eql(
|
|
undefined
|
|
);
|
|
expect(h.uniformLocations_[DefaultUniform.SIZE_PX]).not.to.eql(
|
|
undefined
|
|
);
|
|
expect(h.uniformLocations_[DefaultUniform.PIXEL_RATIO]).not.to.eql(
|
|
undefined
|
|
);
|
|
});
|
|
|
|
it('has processed uniforms', function () {
|
|
expect(h.uniforms_.length).to.eql(4);
|
|
expect(h.uniforms_[0].name).to.eql('u_test1');
|
|
expect(h.uniforms_[1].name).to.eql('u_test2');
|
|
expect(h.uniforms_[2].name).to.eql('u_test3');
|
|
expect(h.uniforms_[3].name).to.eql('u_test4');
|
|
expect(h.uniforms_[0].location).to.not.eql(-1);
|
|
expect(h.uniforms_[1].location).to.not.eql(-1);
|
|
expect(h.uniforms_[2].location).to.not.eql(-1);
|
|
expect(h.uniforms_[3].location).to.not.eql(-1);
|
|
expect(h.uniforms_[2].texture).to.not.eql(undefined);
|
|
});
|
|
});
|
|
|
|
describe('valid shader compiling', function () {
|
|
let p;
|
|
beforeEach(function () {
|
|
h = new WebGLHelper();
|
|
|
|
p = h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER);
|
|
h.useProgram(p, SAMPLE_FRAMESTATE);
|
|
});
|
|
|
|
it('has saved the program', function () {
|
|
expect(h.currentProgram_).to.eql(p);
|
|
});
|
|
|
|
it('has no shader compilation error', function () {
|
|
expect(h.shaderCompileErrors_).to.eql(null);
|
|
});
|
|
|
|
it('can find the uniform location', function () {
|
|
expect(h.getUniformLocation('u_test')).to.not.eql(null);
|
|
});
|
|
|
|
it('can find the attribute location', function () {
|
|
expect(h.getAttributeLocation('a_test')).to.not.eql(-1);
|
|
});
|
|
|
|
it('cannot find an unknown attribute location', function () {
|
|
expect(h.getAttributeLocation('a_test_missing')).to.eql(-1);
|
|
});
|
|
});
|
|
|
|
describe('invalid shader compiling', function () {
|
|
it('throws for an invalid vertex shader', function () {
|
|
h = new WebGLHelper();
|
|
expect(() =>
|
|
h.getProgram(FRAGMENT_SHADER, INVALID_VERTEX_SHADER)
|
|
).to.throwException(
|
|
/Vertex shader compilation failed: ERROR: 0:10: 'bla' : syntax error/
|
|
);
|
|
});
|
|
|
|
it('throws for an invalid fragment shader', function () {
|
|
h = new WebGLHelper();
|
|
expect(() =>
|
|
h.getProgram(INVALID_FRAGMENT_SHADER, VERTEX_SHADER)
|
|
).to.throwException(
|
|
/Fragment shader compliation failed: ERROR: 0:5: 'oops' : undeclared identifier/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('#makeProjectionTransform', function () {
|
|
beforeEach(function () {
|
|
h = new WebGLHelper();
|
|
});
|
|
|
|
it('gives out the correct transform', function () {
|
|
const scaleX =
|
|
2 /
|
|
SAMPLE_FRAMESTATE.size[0] /
|
|
SAMPLE_FRAMESTATE.viewState.resolution;
|
|
const scaleY =
|
|
2 /
|
|
SAMPLE_FRAMESTATE.size[1] /
|
|
SAMPLE_FRAMESTATE.viewState.resolution;
|
|
const given = createTransform();
|
|
const expected = createTransform();
|
|
scaleTransform(expected, scaleX, scaleY);
|
|
rotateTransform(expected, -SAMPLE_FRAMESTATE.viewState.rotation);
|
|
translateTransform(
|
|
expected,
|
|
-SAMPLE_FRAMESTATE.viewState.center[0],
|
|
-SAMPLE_FRAMESTATE.viewState.center[1]
|
|
);
|
|
|
|
h.makeProjectionTransform(SAMPLE_FRAMESTATE, given);
|
|
|
|
expect(given.map((val) => val.toFixed(15))).to.eql(
|
|
expected.map((val) => val.toFixed(15))
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('deleteBuffer()', function () {
|
|
it('can be called to free up buffer resources', function () {
|
|
h = new WebGLHelper();
|
|
const buffer = new WebGLArrayBuffer(ARRAY_BUFFER, STATIC_DRAW);
|
|
buffer.fromArray([0, 1, 2, 3]);
|
|
h.flushBufferData(buffer);
|
|
const bufferKey = getUid(buffer);
|
|
expect(h.bufferCache_).to.have.property(bufferKey);
|
|
|
|
h.deleteBuffer(buffer);
|
|
expect(h.bufferCache_).to.not.have.property(bufferKey);
|
|
});
|
|
});
|
|
|
|
describe('#createTexture', function () {
|
|
beforeEach(function () {
|
|
h = new WebGLHelper();
|
|
});
|
|
|
|
it('creates an empty texture from scratch', function () {
|
|
const width = 4;
|
|
const height = 4;
|
|
const t = h.createTexture([width, height]);
|
|
const gl = h.getGL();
|
|
|
|
const fb = gl.createFramebuffer();
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
t,
|
|
0
|
|
);
|
|
const data = new Uint8Array(width * height * 4);
|
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
gl.deleteFramebuffer(fb);
|
|
|
|
expect(data[0]).to.eql(0);
|
|
expect(data[1]).to.eql(0);
|
|
expect(data[2]).to.eql(0);
|
|
expect(data[3]).to.eql(0);
|
|
expect(data[4]).to.eql(0);
|
|
expect(data[5]).to.eql(0);
|
|
expect(data[6]).to.eql(0);
|
|
expect(data[7]).to.eql(0);
|
|
});
|
|
|
|
it('creates a texture from image data', function () {
|
|
const width = 4;
|
|
const height = 4;
|
|
const canvas = document.createElement('canvas');
|
|
const image = canvas.getContext('2d').createImageData(width, height);
|
|
for (let i = 0; i < image.data.length; i += 4) {
|
|
image.data[i] = 100;
|
|
image.data[i + 1] = 150;
|
|
image.data[i + 2] = 200;
|
|
image.data[i + 3] = 250;
|
|
}
|
|
const t = h.createTexture([width, height], image);
|
|
const gl = h.getGL();
|
|
|
|
const fb = gl.createFramebuffer();
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
t,
|
|
0
|
|
);
|
|
const data = new Uint8Array(width * height * 4);
|
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
gl.deleteFramebuffer(fb);
|
|
|
|
expect(data[0]).to.eql(100);
|
|
expect(data[1]).to.eql(150);
|
|
expect(data[2]).to.eql(200);
|
|
expect(data[3]).to.eql(250);
|
|
expect(data[4]).to.eql(100);
|
|
expect(data[5]).to.eql(150);
|
|
expect(data[6]).to.eql(200);
|
|
expect(data[7]).to.eql(250);
|
|
});
|
|
|
|
it('reuses a given texture', function () {
|
|
const width = 4;
|
|
const height = 4;
|
|
const gl = h.getGL();
|
|
const t1 = gl.createTexture();
|
|
const t2 = h.createTexture([width, height], undefined, t1);
|
|
expect(t1).to.be(t2);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#enableAttributes', function () {
|
|
let baseAttrs;
|
|
|
|
beforeEach(function () {
|
|
h = new WebGLHelper();
|
|
baseAttrs = [
|
|
{
|
|
name: 'attr1',
|
|
size: 3,
|
|
},
|
|
{
|
|
name: 'attr2',
|
|
size: 2,
|
|
},
|
|
{
|
|
name: 'attr3',
|
|
size: 1,
|
|
},
|
|
];
|
|
h.useProgram(
|
|
h.getProgram(
|
|
FRAGMENT_SHADER,
|
|
`
|
|
precision mediump float;
|
|
|
|
uniform mat4 u_projectionMatrix;
|
|
uniform mat4 u_offsetScaleMatrix;
|
|
uniform mat4 u_offsetRotateMatrix;
|
|
|
|
attribute vec3 attr1;
|
|
attribute vec2 attr2;
|
|
attribute float attr3;
|
|
uniform float u_test;
|
|
|
|
void main(void) {
|
|
gl_Position = vec4(u_test, attr3, 0.0, 1.0);
|
|
}`
|
|
),
|
|
SAMPLE_FRAMESTATE
|
|
);
|
|
});
|
|
|
|
it('enables attributes based on the given array (FLOAT)', function () {
|
|
const spy = sinon.spy(h, 'enableAttributeArray_');
|
|
h.enableAttributes(baseAttrs);
|
|
const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
|
|
|
|
expect(spy.callCount).to.eql(3);
|
|
expect(spy.getCall(0).args[0]).to.eql('attr1');
|
|
expect(spy.getCall(0).args[1]).to.eql(3);
|
|
expect(spy.getCall(0).args[2]).to.eql(FLOAT);
|
|
expect(spy.getCall(0).args[3]).to.eql(6 * bytesPerFloat);
|
|
expect(spy.getCall(0).args[4]).to.eql(0);
|
|
expect(spy.getCall(1).args[0]).to.eql('attr2');
|
|
expect(spy.getCall(1).args[1]).to.eql(2);
|
|
expect(spy.getCall(1).args[2]).to.eql(FLOAT);
|
|
expect(spy.getCall(1).args[3]).to.eql(6 * bytesPerFloat);
|
|
expect(spy.getCall(1).args[4]).to.eql(3 * bytesPerFloat);
|
|
expect(spy.getCall(2).args[0]).to.eql('attr3');
|
|
expect(spy.getCall(2).args[1]).to.eql(1);
|
|
expect(spy.getCall(2).args[2]).to.eql(FLOAT);
|
|
expect(spy.getCall(2).args[3]).to.eql(6 * bytesPerFloat);
|
|
expect(spy.getCall(2).args[4]).to.eql(5 * bytesPerFloat);
|
|
});
|
|
});
|
|
|
|
describe('#applyFrameState', function () {
|
|
let stubMatrix, stubFloat, stubVec2, stubTime;
|
|
beforeEach(function () {
|
|
stubTime = sinon.stub(Date, 'now');
|
|
stubTime.returns(1000);
|
|
h = new WebGLHelper();
|
|
stubMatrix = sinon.stub(h, 'setUniformMatrixValue');
|
|
stubFloat = sinon.stub(h, 'setUniformFloatValue');
|
|
stubVec2 = sinon.stub(h, 'setUniformFloatVec2');
|
|
|
|
stubTime.returns(2000);
|
|
h.applyFrameState({...SAMPLE_FRAMESTATE, pixelRatio: 2});
|
|
});
|
|
|
|
afterEach(function () {
|
|
stubTime.restore();
|
|
});
|
|
|
|
it('sets the default uniforms according the frame state', function () {
|
|
expect(stubMatrix.getCall(0).args).to.eql([
|
|
DefaultUniform.OFFSET_SCALE_MATRIX,
|
|
[
|
|
0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
|
|
0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
|
|
],
|
|
]);
|
|
expect(stubMatrix.getCall(1).args).to.eql([
|
|
DefaultUniform.OFFSET_ROTATION_MATRIX,
|
|
[
|
|
0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
|
|
0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
|
|
],
|
|
]);
|
|
|
|
expect(stubFloat.getCall(0).args).to.eql([DefaultUniform.TIME, 1]);
|
|
expect(stubFloat.getCall(1).args).to.eql([DefaultUniform.ZOOM, 3]);
|
|
expect(stubFloat.getCall(2).args).to.eql([DefaultUniform.RESOLUTION, 2]);
|
|
expect(stubFloat.getCall(3).args).to.eql([DefaultUniform.PIXEL_RATIO, 2]);
|
|
|
|
expect(stubVec2.getCall(0).args).to.eql([
|
|
DefaultUniform.SIZE_PX,
|
|
[100, 150],
|
|
]);
|
|
});
|
|
});
|
|
});
|