mirror of
https://github.com/visgl/luma.gl.git
synced 2026-01-25 14:08:58 +00:00
609 lines
17 KiB
TypeScript
609 lines
17 KiB
TypeScript
// luma.gl
|
|
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) vis.gl contributors
|
|
|
|
import test from 'tape-promise/tape';
|
|
import {luma} from '@luma.gl/core';
|
|
import {Model, PipelineFactory, ShaderFactory} from '@luma.gl/engine';
|
|
import {getWebGLTestDevice, getTestDevices} from '@luma.gl/test-utils';
|
|
|
|
const stats = luma.stats.get('Resource Counts');
|
|
|
|
const DUMMY_WGSL = /* WGSL */ `
|
|
@vertex fn vertexMain() -> @builtin(position) vec4<f32> {
|
|
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
}
|
|
|
|
@fragment fn fragmentMain(@builtin(position) coord_in: vec4<f32>) -> @location(0) vec4<f32> {
|
|
return vec4<f32>(coord_in.x, coord_in.y, 0.0, 1.0);
|
|
}
|
|
`;
|
|
|
|
const DUMMY_VS = `#version 300 es
|
|
void main() { gl_Position = vec4(1.0); }
|
|
`;
|
|
|
|
const DUMMY_FS = `#version 300 es
|
|
precision highp float;
|
|
out vec4 fragColor;
|
|
void main() { fragColor = vec4(1.0); }
|
|
`;
|
|
|
|
const mockModule = {
|
|
name: 'test-module',
|
|
vs: '',
|
|
fs: '',
|
|
getUniforms: (opts, context) => ({}),
|
|
dependencies: []
|
|
};
|
|
|
|
test('Model#construct/destruct', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
|
|
const model = new Model(webglDevice, {
|
|
id: 'construct-destruct-test',
|
|
topology: 'point-list',
|
|
vertexCount: 0,
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS
|
|
});
|
|
|
|
t.ok(model, 'Model constructor does not throw errors');
|
|
t.ok(model.id, 'Model has an id');
|
|
t.ok(model.pipeline, 'Created pipeline');
|
|
|
|
model.destroy();
|
|
t.true(model.pipeline.destroyed, 'Deleted pipeline');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#multiple delete', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
|
|
const model1 = new Model(webglDevice, {
|
|
id: 'multiple-delete-test-1',
|
|
topology: 'point-list',
|
|
vertexCount: 0,
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS
|
|
});
|
|
|
|
const model2 = new Model(webglDevice, {
|
|
id: 'multiple-delete-test-2',
|
|
topology: 'point-list',
|
|
vertexCount: 0,
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS
|
|
});
|
|
|
|
model1.destroy();
|
|
t.ok(model2.pipeline.destroyed === false, 'program still in use');
|
|
model1.destroy();
|
|
t.ok(model2.pipeline.destroyed === false, 'program still in use');
|
|
model2.destroy();
|
|
t.ok(model2.pipeline.destroyed === true, 'program is released');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#setAttributes', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
|
|
const buffer1 = webglDevice.createBuffer({data: new Float32Array(9).fill(0)});
|
|
const buffer2 = webglDevice.createBuffer({data: new Float32Array(9).fill(1)});
|
|
|
|
const initialActiveBuffers = stats.get('Buffers Active').count;
|
|
|
|
const model = new Model(webglDevice, {
|
|
id: 'set-attributes-test',
|
|
vs: `#version 300 es
|
|
in vec4 positions;
|
|
in vec3 normals;
|
|
void main() { gl_Position = positions + vec4(normals, 0.); }
|
|
`,
|
|
fs: DUMMY_FS,
|
|
attributes: {
|
|
positions: webglDevice.createBuffer({data: new Float32Array(12).fill(2)}),
|
|
normals: webglDevice.createBuffer({data: new Float32Array(12).fill(3)})
|
|
},
|
|
bufferLayout: [
|
|
{name: 'positions', format: 'float32x3'},
|
|
{name: 'normals', format: 'float32x3'},
|
|
{name: 'texCoords', format: 'float32x2'}
|
|
]
|
|
});
|
|
|
|
t.is(
|
|
stats.get('Buffers Active').count - initialActiveBuffers,
|
|
2,
|
|
'Created new buffers for attributes'
|
|
);
|
|
|
|
model.setAttributes({positions: buffer1, normals: buffer2});
|
|
|
|
t.deepEqual(model.bufferAttributes, {}, 'no longer stores local attributes');
|
|
|
|
t.is(stats.get('Buffers Active').count - initialActiveBuffers, 2, 'Did not create new buffers');
|
|
|
|
model.destroy();
|
|
|
|
buffer1.destroy();
|
|
buffer2.destroy();
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#setters, getters', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
const model = new Model(webglDevice, {
|
|
id: 'setters-getters-test',
|
|
topology: 'point-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS
|
|
});
|
|
|
|
model.setVertexCount(12);
|
|
t.is(model.vertexCount, 12, 'set vertex count');
|
|
|
|
model.setInstanceCount(4);
|
|
t.is(model.instanceCount, 4, 'set instance count');
|
|
|
|
model.setTopology('triangle-list');
|
|
t.is(model.topology, 'triangle-list', 'set topology');
|
|
|
|
model.destroy();
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#draw', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
|
|
const model = new Model(webglDevice, {
|
|
id: 'draw-test',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
attributes: {
|
|
positions: webglDevice.createBuffer({data: new Float32Array(12).fill(2)}),
|
|
normals: webglDevice.createBuffer({data: new Float32Array(12).fill(3)})
|
|
},
|
|
bufferLayout: [
|
|
{name: 'positions', format: 'float32x3'},
|
|
{name: 'normals', format: 'float32x3'}
|
|
]
|
|
});
|
|
|
|
const renderPass = webglDevice.beginRenderPass({clearColor: [0, 0, 0, 0]});
|
|
|
|
model.draw(renderPass);
|
|
|
|
renderPass.destroy();
|
|
|
|
model.destroy();
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#topology', async t => {
|
|
for (const device of await getTestDevices()) {
|
|
const model = new Model(device, {
|
|
id: 'topology-test',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
source: DUMMY_WGSL,
|
|
vertexEntryPoint: 'vertexMain',
|
|
fragmentEntryPoint: 'fragmentMain',
|
|
vertexCount: 3
|
|
});
|
|
|
|
t.equal(model.topology, 'triangle-list', 'Pipeline has triangle-list topology');
|
|
if (device.type === 'webgpu') {
|
|
// Cached model in WebGL can have a different topology
|
|
t.equal(
|
|
model.pipeline.props.topology,
|
|
'triangle-list',
|
|
'Pipeline has triangle-list topology'
|
|
);
|
|
}
|
|
|
|
model.setTopology('line-strip');
|
|
|
|
const framebuffer = device
|
|
.getDefaultCanvasContext()
|
|
.getCurrentFramebuffer({depthStencilFormat: false});
|
|
const renderPass = device.beginRenderPass({framebuffer, clearColor: [0, 0, 0, 0]});
|
|
model.draw(renderPass);
|
|
|
|
t.equal(model.topology, 'line-strip', 'Pipeline has line-strip topology');
|
|
if (device.type === 'webgpu') {
|
|
// Cached model in WebGL can have a different topology
|
|
t.equal(model.pipeline.props.topology, 'line-strip', 'Pipeline has triangle-list topology');
|
|
}
|
|
|
|
renderPass.end();
|
|
device.submit();
|
|
renderPass.destroy();
|
|
model.destroy();
|
|
}
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#pipeline caching', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
if (!webglDevice.props._cachePipelines) {
|
|
t.comment('Pipeline caching is disabled');
|
|
t.end();
|
|
return;
|
|
}
|
|
|
|
const pipelineFactory = new PipelineFactory(webglDevice);
|
|
const shaderFactory = new ShaderFactory(webglDevice);
|
|
|
|
const model1 = new Model(webglDevice, {
|
|
id: 'pipeline-caching-test-1',
|
|
pipelineFactory,
|
|
shaderFactory,
|
|
topology: 'point-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
uniforms: {x: 0.5}
|
|
});
|
|
|
|
const model2 = new Model(webglDevice, {
|
|
id: 'pipeline-caching-test-2',
|
|
pipelineFactory,
|
|
shaderFactory,
|
|
topology: 'point-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
uniforms: {x: -0.5}
|
|
});
|
|
|
|
t.ok(model1.pipeline === model2.pipeline, 'Pipelines are shared.');
|
|
|
|
const renderPass = webglDevice.beginRenderPass({clearColor: [0, 0, 0, 0]});
|
|
|
|
const uniforms: Record<string, unknown> = {};
|
|
|
|
model1.draw(renderPass);
|
|
t.deepEqual(uniforms, {x: 0.5}, 'Pipeline uniforms set');
|
|
|
|
model2.draw(renderPass);
|
|
t.deepEqual(uniforms, {x: -0.5}, 'Pipeline uniforms set');
|
|
|
|
model2.setBufferLayout([{name: 'a', format: 'float32x3'}]);
|
|
model2.predraw(); // Forces a pipeline update
|
|
t.ok(model1.pipeline !== model2.pipeline, 'Pipeline updated');
|
|
|
|
model2.draw(renderPass);
|
|
t.deepEqual(uniforms, {x: -0.5}, 'Pipeline uniforms set');
|
|
|
|
renderPass.destroy();
|
|
|
|
model1.destroy();
|
|
model2.destroy();
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Model#pipeline caching with defines and modules', async t => {
|
|
const webglDevice = await getWebGLTestDevice();
|
|
if (!webglDevice.props._cachePipelines) {
|
|
t.comment('Pipeline caching is disabled');
|
|
t.end();
|
|
return;
|
|
}
|
|
|
|
const pipelineFactory = PipelineFactory.getDefaultPipelineFactory(webglDevice);
|
|
const shaderFactory = ShaderFactory.getDefaultShaderFactory(webglDevice);
|
|
const model1 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-1',
|
|
topology: 'triangle-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS
|
|
});
|
|
|
|
t.ok(model1.pipeline, 'Got a pipeline');
|
|
|
|
// reuse assembled shaders; this cache is already tested in shader-factory.spec.ts.
|
|
const vs = shaderFactory.createShader({stage: 'vertex', source: model1.vs});
|
|
const fs = shaderFactory.createShader({stage: 'fragment', source: model1.fs});
|
|
|
|
const pipeline2 = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
t.ok(model1.pipeline === pipeline2, 'Got cached pipeline');
|
|
|
|
const defineModel1 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-2',
|
|
topology: 'triangle-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
defines: {MY_DEFINE: true}
|
|
});
|
|
|
|
t.ok(model1.pipeline !== defineModel1.pipeline, 'Define triggers new pipeline');
|
|
|
|
const defineModel2 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-3',
|
|
topology: 'triangle-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
defines: {MY_DEFINE: true}
|
|
});
|
|
|
|
t.ok(defineModel1.pipeline === defineModel2.pipeline, 'Got cached pipeline with defines');
|
|
|
|
const moduleModel1 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-4',
|
|
topology: 'triangle-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
modules: [mockModule]
|
|
});
|
|
|
|
t.ok(model1.pipeline !== moduleModel1.pipeline, 'Module triggers new pipeline');
|
|
t.ok(defineModel1.pipeline !== moduleModel1.pipeline, 'Module triggers new pipeline');
|
|
|
|
const moduleModel2 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-5',
|
|
topology: 'triangle-list',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
modules: [mockModule]
|
|
});
|
|
|
|
t.ok(moduleModel1.pipeline === moduleModel2.pipeline, 'Got cached pipeline with modules');
|
|
|
|
const defineModuleModel1 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-6',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
topology: 'triangle-list',
|
|
modules: [mockModule],
|
|
defines: {MY_DEFINE: true}
|
|
});
|
|
|
|
t.ok(pipeline2 !== defineModuleModel1.pipeline, 'Module and define triggers new pipeline');
|
|
t.ok(
|
|
defineModel1.pipeline !== defineModuleModel1.pipeline,
|
|
'Module and define triggers new pipeline'
|
|
);
|
|
t.ok(
|
|
moduleModel1.pipeline !== defineModuleModel1.pipeline,
|
|
'Module and define triggers new pipeline'
|
|
);
|
|
|
|
const defineModuleModel2 = new Model(webglDevice, {
|
|
id: 'caching-with-modules-test-7',
|
|
vs: DUMMY_VS,
|
|
fs: DUMMY_FS,
|
|
topology: 'triangle-list',
|
|
modules: [mockModule],
|
|
defines: {MY_DEFINE: true}
|
|
});
|
|
|
|
t.ok(
|
|
defineModuleModel1.pipeline === defineModuleModel2.pipeline,
|
|
'Got cached pipeline with modules and defines'
|
|
);
|
|
|
|
t.end();
|
|
});
|
|
|
|
/*
|
|
import {dirlight, picking} from '@luma.gl/shadertools';
|
|
|
|
const VS_300 = glsl`#version 300 es
|
|
|
|
in vec4 positions;
|
|
in vec2 uvs;
|
|
|
|
out vec2 vUV;
|
|
|
|
void main() {
|
|
vUV = uvs;
|
|
gl_Position = positions;
|
|
}
|
|
`;
|
|
|
|
const FS_300 = glsl`#version 300 es
|
|
precision highp float;
|
|
|
|
in vec2 vUV;
|
|
|
|
uniform sampler2D tex;
|
|
|
|
out vec4 fragColor;
|
|
void main() {
|
|
fragColor = texture(tex, vUV);
|
|
}
|
|
`;
|
|
|
|
|
|
// TODO - Move to model: transpilation functionality was moved to model
|
|
test('PipelineFactory#hooks', (t) => {
|
|
const pipelineFactory = new PipelineFactory(webglDevice);
|
|
|
|
const preHookPipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
pipelineFactory.addShaderHook('vs:LUMAGL_pickColor(inout vec4 color)');
|
|
pipelineFactory.addShaderHook('fs:LUMAGL_fragmentColor(inout vec4 color)', {
|
|
header: 'if (color.a == 0.0) discard;\n',
|
|
footer: 'color.a *= 1.2;\n'
|
|
});
|
|
|
|
const postHookPipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
t.ok(preHookPipeline !== postHookPipeline, 'Adding hooks changes hash');
|
|
|
|
const pickingInjection = Object.assign(
|
|
{
|
|
inject: {
|
|
'vs:LUMAGL_pickColor': 'picking_setPickingColor(color.rgb);',
|
|
'fs:LUMAGL_fragmentColor': {
|
|
injection: 'color = picking_filterColor(color);',
|
|
order: Number.POSITIVE_INFINITY
|
|
}
|
|
}
|
|
},
|
|
picking
|
|
);
|
|
|
|
const noModulePipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
t.ok(preHookPipeline !== noModulePipeline, 'Adding hooks changes hash');
|
|
|
|
const noModuleVs = noModulePipeline.vs.source;
|
|
const noModuleFs = noModulePipeline.fs.source;
|
|
|
|
t.ok(noModuleVs.indexOf('LUMAGL_pickColor') > -1, 'hook function injected into vertex shader');
|
|
t.ok(
|
|
noModuleFs.indexOf('LUMAGL_fragmentColor') > -1,
|
|
'hook function injected into fragment shader'
|
|
);
|
|
|
|
t.ok(
|
|
noModuleVs.indexOf('picking_setPickingColor(color.rgb)') === -1,
|
|
'injection code not included in vertex shader without module'
|
|
);
|
|
t.ok(
|
|
noModuleFs.indexOf('color = picking_filterColor(color)') === -1,
|
|
'injection code not included in fragment shader without module'
|
|
);
|
|
|
|
const modulesPipeline = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
modules: [pickingInjection]
|
|
});
|
|
const modulesVs = modulesPipeline.vs.source;
|
|
const modulesFs = modulesPipeline.fs.source;
|
|
|
|
t.ok(modulesVs.indexOf('LUMAGL_pickColor') > -1, 'hook function injected into vertex shader');
|
|
t.ok(
|
|
modulesFs.indexOf('LUMAGL_fragmentColor') > -1,
|
|
'hook function injected into fragment shader'
|
|
);
|
|
|
|
t.ok(
|
|
modulesVs.indexOf('picking_setPickingColor(color.rgb)') > -1,
|
|
'injection code included in vertex shader with module'
|
|
);
|
|
t.ok(
|
|
modulesFs.indexOf('color = picking_filterColor(color)') > -1,
|
|
'injection code included in fragment shader with module'
|
|
);
|
|
t.ok(
|
|
modulesFs.indexOf('if (color.a == 0.0) discard;') > -1,
|
|
'hook header injected into fragment shader'
|
|
);
|
|
t.ok(
|
|
modulesFs.indexOf('color.a *= 1.2;') > modulesFs.indexOf('color = picking_filterColor(color)'),
|
|
'hook footer injected after injection code'
|
|
);
|
|
|
|
const injectPipeline = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
inject: {
|
|
'vs:LUMAGL_pickColor': 'color *= 0.1;',
|
|
'fs:LUMAGL_fragmentColor': 'color += 0.1;'
|
|
}
|
|
});
|
|
const injectVs = injectPipeline.vs.source;
|
|
const injectFs = injectPipeline.fs.source;
|
|
|
|
t.ok(injectVs.indexOf('color *= 0.1') > -1, 'argument injection code included in shader hook');
|
|
t.ok(injectFs.indexOf('color += 0.1') > -1, 'argument injection code included in shader hook');
|
|
|
|
const injectDefinePipeline1 = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
inject: {
|
|
'vs:LUMAGL_pickColor': 'color *= 0.1;'
|
|
}
|
|
});
|
|
|
|
const injectDefinePipeline2 = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
defines: {
|
|
'vs:LUMAGL_pickColor': 'color *= 0.1;'
|
|
}
|
|
});
|
|
|
|
t.ok(injectDefinePipeline1 !== injectDefinePipeline2, 'Injects and defines hashed separately.');
|
|
|
|
t.end();
|
|
});
|
|
|
|
// TODO - Move to model: transpilation functionality was moved to model
|
|
test('PipelineFactory#defaultModules', (t) => {
|
|
const pipelineFactory = new PipelineFactory(webglDevice);
|
|
|
|
const {pipeline} = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
const preDefaultModulePipeline = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
modules: [dirlight]
|
|
});
|
|
|
|
const preDefaultModuleSource = preDefaultModulePipeline.fs.source;
|
|
|
|
pipelineFactory.addDefaultModule(dirlight);
|
|
|
|
const defaultModulePipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
const modulePipeline = pipelineFactory.createRenderPipeline({
|
|
vs,
|
|
fs,
|
|
topology: 'triangle-list',
|
|
modules: [dirlight]
|
|
});
|
|
|
|
t.ok(pipeline !== defaultModulePipeline, 'Pipeline with new default module properly cached');
|
|
t.ok(
|
|
preDefaultModulePipeline !== defaultModulePipeline,
|
|
'Adding a default module changes the pipeline hash'
|
|
);
|
|
t.ok(
|
|
preDefaultModulePipeline.fs.source === defaultModulePipeline.fs.source,
|
|
'Default module injected correctly'
|
|
);
|
|
t.ok(
|
|
modulePipeline === defaultModulePipeline,
|
|
'Pipeline with new default module matches regular module'
|
|
);
|
|
|
|
pipelineFactory.removeDefaultModule(dirlight);
|
|
|
|
const noDefaultModulePipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
|
|
t.ok(pipeline.fs.source === noDefaultModulePipeline.fs.source, 'Default module was removed');
|
|
t.ok(modulePipeline.fs.source !== noDefaultModulePipeline.fs.source, 'Default module was removed');
|
|
|
|
// Reset pipeline manager
|
|
pipelineFactory.release(pipeline);
|
|
pipelineFactory.release(modulePipeline);
|
|
pipelineFactory.release(defaultModulePipeline);
|
|
pipelineFactory.release(noDefaultModulePipeline);
|
|
|
|
pipelineFactory.addDefaultModule(dirlight);
|
|
const uncachedPipeline = pipelineFactory.createRenderPipeline({vs, fs, topology: 'triangle-list'});
|
|
const defaultModuleSource = uncachedPipeline.fs.source;
|
|
|
|
t.ok(defaultModulePipeline !== uncachedPipeline, 'Pipeline is not cached');
|
|
t.ok(preDefaultModuleSource === defaultModuleSource, 'Default modules create correct source');
|
|
|
|
t.end();
|
|
});
|
|
|
|
*/
|