Skip to content

Commit

Permalink
Dual-Stack WebGL Runtime with WebGL2 to WebGL1 Fallback (#5198)
Browse files Browse the repository at this point in the history
* refactor: WebGL2 to WebGL1 fallback at runtime

* update changelog

* update changelog

* replace isWebGL2 check

* revert program.ts

* implement shaders.transpileToWebGL1(), refactor shaders.prepare()

* revert readme

* add `shaders.test.ts`

* more shader tests

* update changelog

* fix shader transpileToWebGL1 throw test

* addressed code review feedback
  • Loading branch information
0xFA11 authored Dec 16, 2024
1 parent 414e474 commit 5ad1709
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 93 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
## main

### ✨ Features and improvements

- Add support for projection type expression as part of a refactoring of the transfrom and projection classes ([#5139](https://github.com/maplibre/maplibre-gl-js/pull/5139))
- ⚠️ Support setting WebGL context options on map creation ([#5196](https://github.com/maplibre/maplibre-gl-js/pull/5196)). Previously supported WebGL context options like `antialias`, `preserveDrawingBuffer` and `failIfMajorPerformanceCaveat` must now be defined inside the `canvasContextAttributes` object on `MapOptions`.
- Dual-Stack WebGL Runtime with WebGL2 to WebGL1 Fallback ([#5198](https://github.com/maplibre/maplibre-gl-js/pull/5198))
- _...Add new stuff here..._

### 🐞 Bug fixes

- Fix globe custom layers being supplied incorrect matrices after projection transition to mercator ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150))
- Fix custom 3D models disappearing during projection transition ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150))
- Fix regression in NavigationControl compass on Firefox and Safari browsers ([#5205](https://github.com/maplibre/maplibre-gl-js/pull/5205))
Expand Down
56 changes: 12 additions & 44 deletions build/generate-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,9 @@ console.log('Generating shaders');
* It will also create a simple package.json file to allow importing this package in webpack
*/

const vertex = globSync('./src/shaders/*.vertex.glsl');
for (const file of vertex) {
const code = fs.readFileSync(file, 'utf8');
const content = glslToTs(code, 'vertex');
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, content);
}

console.log(`Finished converting ${vertex.length} vertex shaders`);

const fragment = globSync('./src/shaders/*.fragment.glsl');
for (const file of fragment) {
const code = fs.readFileSync(file, 'utf8');
const content = glslToTs(code, 'fragment');
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, content);
}

console.log(`Finished converting ${fragment.length} fragment shaders`);

function glslToTs(code: string, type: 'fragment'|'vertex'): string {
code = code
.trim(); // strip whitespace at the start/end

// WebGL1 Compat -- Start

if (type === 'fragment') {
code = code
.replace(/\bin\s/g, 'varying ') // For fragment shaders, replace "in " with "varying "
.replace('out highp vec4 fragColor;', '');
}

if (type === 'vertex') {
code = code
.replace(/\bin\s/g, 'attribute ') // For vertex shaders, replace "in " with "attribute "
.replace(/\bout\s/g, 'varying '); // For vertex shaders, replace "out " with "varying "
}

code = code
.replace(/fragColor/g, 'gl_FragColor')
.replace(/texture\(/g, 'texture2D(');

// WebGL1 Compat -- End

function glslToTs(code: string): string {
code = code
.trim() // strip whitespace at the start/end
.replace(/\s*\/\/[^\n]*\n/g, '\n') // strip double-slash comments
.replace(/\n+/g, '\n') // collapse multi line breaks
.replace(/\n\s+/g, '\n') // strip indentation
Expand All @@ -66,3 +24,13 @@ function glslToTs(code: string, type: 'fragment'|'vertex'): string {
return `// This file is generated. Edit build/generate-shaders.ts, then run \`npm run codegen\`.
export default ${JSON.stringify(code).replaceAll('"', '\'')};\n`;
}

const shaderFiles = globSync('./src/shaders/*.glsl');
for (const file of shaderFiles) {
const glslFile = fs.readFileSync(file, 'utf8');
const tsSource = glslToTs(glslFile);
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, tsSource);
}

console.log(`Finished converting ${shaderFiles.length} shaders`);
15 changes: 12 additions & 3 deletions src/render/program.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {type PreparedShader, shaders} from '../shaders/shaders';
import {type PreparedShader, shaders, transpileVertexShaderToWebGL1, transpileFragmentShaderToWebGL1} from '../shaders/shaders';
import {type ProgramConfiguration} from '../data/program_configuration';
import {VertexArrayObject} from './vertex_array_object';
import {type Context} from '../gl/context';
import {isWebGL2} from '../gl/webgl2';

import type {SegmentVector} from '../data/segment';
import type {VertexBuffer} from '../gl/vertex_buffer';
Expand Down Expand Up @@ -72,6 +73,9 @@ export class Program<Us extends UniformBindings> {
}

const defines = configuration ? configuration.defines() : [];
if (isWebGL2(gl)) {
defines.unshift('#version 300 es');
}
if (showOverdrawInspector) {
defines.push('#define OVERDRAW_INSPECTOR;');
}
Expand All @@ -82,8 +86,13 @@ export class Program<Us extends UniformBindings> {
defines.push(projectionDefine);
}

const fragmentSource = defines.concat(shaders.prelude.fragmentSource, projectionPrelude.fragmentSource, source.fragmentSource).join('\n');
const vertexSource = defines.concat(shaders.prelude.vertexSource, projectionPrelude.vertexSource, source.vertexSource).join('\n');
let fragmentSource = defines.concat(shaders.prelude.fragmentSource, projectionPrelude.fragmentSource, source.fragmentSource).join('\n');
let vertexSource = defines.concat(shaders.prelude.vertexSource, projectionPrelude.vertexSource, source.vertexSource).join('\n');

if (!isWebGL2(gl)) {
fragmentSource = transpileFragmentShaderToWebGL1(fragmentSource);
vertexSource = transpileVertexShaderToWebGL1(vertexSource);
}

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
if (gl.isContextLost()) {
Expand Down
107 changes: 61 additions & 46 deletions src/shaders/shaders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// Disable Flow annotations here because Flow doesn't support importing GLSL files

import preludeFrag from './_prelude.fragment.glsl.g';
Expand Down Expand Up @@ -77,52 +76,51 @@ export type PreparedShader = {
};

export const shaders = {
prelude: compile(preludeFrag, preludeVert),
projectionMercator: compile('', projectionMercatorVert),
projectionGlobe: compile('', projectionGlobeVert),
background: compile(backgroundFrag, backgroundVert),
backgroundPattern: compile(backgroundPatternFrag, backgroundPatternVert),
circle: compile(circleFrag, circleVert),
clippingMask: compile(clippingMaskFrag, clippingMaskVert),
heatmap: compile(heatmapFrag, heatmapVert),
heatmapTexture: compile(heatmapTextureFrag, heatmapTextureVert),
collisionBox: compile(collisionBoxFrag, collisionBoxVert),
collisionCircle: compile(collisionCircleFrag, collisionCircleVert),
debug: compile(debugFrag, debugVert),
depth: compile(clippingMaskFrag, depthVert),
fill: compile(fillFrag, fillVert),
fillOutline: compile(fillOutlineFrag, fillOutlineVert),
fillOutlinePattern: compile(fillOutlinePatternFrag, fillOutlinePatternVert),
fillPattern: compile(fillPatternFrag, fillPatternVert),
fillExtrusion: compile(fillExtrusionFrag, fillExtrusionVert),
fillExtrusionPattern: compile(fillExtrusionPatternFrag, fillExtrusionPatternVert),
hillshadePrepare: compile(hillshadePrepareFrag, hillshadePrepareVert),
hillshade: compile(hillshadeFrag, hillshadeVert),
line: compile(lineFrag, lineVert),
lineGradient: compile(lineGradientFrag, lineGradientVert),
linePattern: compile(linePatternFrag, linePatternVert),
lineSDF: compile(lineSDFFrag, lineSDFVert),
raster: compile(rasterFrag, rasterVert),
symbolIcon: compile(symbolIconFrag, symbolIconVert),
symbolSDF: compile(symbolSDFFrag, symbolSDFVert),
symbolTextAndIcon: compile(symbolTextAndIconFrag, symbolTextAndIconVert),
terrain: compile(terrainFrag, terrainVert),
terrainDepth: compile(terrainDepthFrag, terrainVertDepth),
terrainCoords: compile(terrainCoordsFrag, terrainVertCoords),
projectionErrorMeasurement: compile(projectionErrorMeasurementFrag, projectionErrorMeasurementVert),
atmosphere: compile(atmosphereFrag, atmosphereVert),
sky: compile(skyFrag, skyVert),
prelude: prepare(preludeFrag, preludeVert),
projectionMercator: prepare('', projectionMercatorVert),
projectionGlobe: prepare('', projectionGlobeVert),
background: prepare(backgroundFrag, backgroundVert),
backgroundPattern: prepare(backgroundPatternFrag, backgroundPatternVert),
circle: prepare(circleFrag, circleVert),
clippingMask: prepare(clippingMaskFrag, clippingMaskVert),
heatmap: prepare(heatmapFrag, heatmapVert),
heatmapTexture: prepare(heatmapTextureFrag, heatmapTextureVert),
collisionBox: prepare(collisionBoxFrag, collisionBoxVert),
collisionCircle: prepare(collisionCircleFrag, collisionCircleVert),
debug: prepare(debugFrag, debugVert),
depth: prepare(clippingMaskFrag, depthVert),
fill: prepare(fillFrag, fillVert),
fillOutline: prepare(fillOutlineFrag, fillOutlineVert),
fillOutlinePattern: prepare(fillOutlinePatternFrag, fillOutlinePatternVert),
fillPattern: prepare(fillPatternFrag, fillPatternVert),
fillExtrusion: prepare(fillExtrusionFrag, fillExtrusionVert),
fillExtrusionPattern: prepare(fillExtrusionPatternFrag, fillExtrusionPatternVert),
hillshadePrepare: prepare(hillshadePrepareFrag, hillshadePrepareVert),
hillshade: prepare(hillshadeFrag, hillshadeVert),
line: prepare(lineFrag, lineVert),
lineGradient: prepare(lineGradientFrag, lineGradientVert),
linePattern: prepare(linePatternFrag, linePatternVert),
lineSDF: prepare(lineSDFFrag, lineSDFVert),
raster: prepare(rasterFrag, rasterVert),
symbolIcon: prepare(symbolIconFrag, symbolIconVert),
symbolSDF: prepare(symbolSDFFrag, symbolSDFVert),
symbolTextAndIcon: prepare(symbolTextAndIconFrag, symbolTextAndIconVert),
terrain: prepare(terrainFrag, terrainVert),
terrainDepth: prepare(terrainDepthFrag, terrainVertDepth),
terrainCoords: prepare(terrainCoordsFrag, terrainVertCoords),
projectionErrorMeasurement: prepare(projectionErrorMeasurementFrag, projectionErrorMeasurementVert),
atmosphere: prepare(atmosphereFrag, atmosphereVert),
sky: prepare(skyFrag, skyVert),
};

// Expand #pragmas to #ifdefs.

function compile(fragmentSource: string, vertexSource: string): PreparedShader {
/** Expand #pragmas to #ifdefs, extract attributes and uniforms */
function prepare(fragmentSource: string, vertexSource: string): PreparedShader {
const re = /#pragma mapbox: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g;

const staticAttributes = vertexSource.match(/attribute ([\w]+) ([\w]+)/g);
const vertexAttributes = vertexSource.match(/in ([\w]+) ([\w]+)/g);
const fragmentUniforms = fragmentSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g);
const vertexUniforms = vertexSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g);
const staticUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms;
const shaderUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms;

const fragmentPragmas = {};

Expand All @@ -131,7 +129,7 @@ function compile(fragmentSource: string, vertexSource: string): PreparedShader {
if (operation === 'define') {
return `
#ifndef HAS_UNIFORM_u_${name}
varying ${precision} ${type} ${name};
in ${precision} ${type} ${name};
#else
uniform ${precision} ${type} u_${name};
#endif
Expand All @@ -154,8 +152,8 @@ uniform ${precision} ${type} u_${name};
return `
#ifndef HAS_UNIFORM_u_${name}
uniform lowp float u_${name}_t;
attribute ${precision} ${attrType} a_${name};
varying ${precision} ${type} ${name};
in ${precision} ${attrType} a_${name};
out ${precision} ${type} ${name};
#else
uniform ${precision} ${type} u_${name};
#endif
Expand Down Expand Up @@ -185,7 +183,7 @@ uniform ${precision} ${type} u_${name};
return `
#ifndef HAS_UNIFORM_u_${name}
uniform lowp float u_${name}_t;
attribute ${precision} ${attrType} a_${name};
in ${precision} ${attrType} a_${name};
#else
uniform ${precision} ${type} u_${name};
#endif
Expand Down Expand Up @@ -213,5 +211,22 @@ uniform ${precision} ${type} u_${name};
}
});

return {fragmentSource, vertexSource, staticAttributes, staticUniforms};
return {fragmentSource, vertexSource, staticAttributes: vertexAttributes, staticUniforms: shaderUniforms};
}

/** Transpile WebGL2 vertex shader source to WebGL1 */
export function transpileVertexShaderToWebGL1(source: string): string {
return source
.replace(/\bin\s/g, 'attribute ')
.replace(/\bout\s/g, 'varying ')
.replace(/texture\(/g, 'texture2D(');
}

/** Transpile WebGL2 fragment shader source to WebGL1 */
export function transpileFragmentShaderToWebGL1(source: string): string {
return source
.replace(/\bin\s/g, 'varying ')
.replace('out highp vec4 fragColor;', '')
.replace(/fragColor/g, 'gl_FragColor')
.replace(/texture\(/g, 'texture2D(');
}
48 changes: 48 additions & 0 deletions test/build/shaders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {transpileVertexShaderToWebGL1, transpileFragmentShaderToWebGL1} from '../../src/shaders/shaders';
import {describe, test, expect} from 'vitest';
import {globSync} from 'glob';
import fs from 'fs';

describe('Shaders', () => {
test('webgl2 to webgl1 transpiled shaders should be identical', () => {
const vertexSourceWebGL2 = `
in vec3 aPos;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * vec4(aPos, 1.0);
gl_PointSize = 20.0;
}`;
const fragmentSourceWebGL2 = `
out highp vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`;
const vertexSourceWebGL1 = `
attribute vec3 aPos;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * vec4(aPos, 1.0);
gl_PointSize = 20.0;
}`;
const fragmentSourceWebGL1 = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`;
const vertexSourceTranspiled = transpileVertexShaderToWebGL1(vertexSourceWebGL2);
const fragmentSourceTranspiled = transpileFragmentShaderToWebGL1(fragmentSourceWebGL2);
expect(vertexSourceTranspiled.trim()).equals(vertexSourceWebGL1.trim());
expect(fragmentSourceTranspiled.trim()).equals(fragmentSourceWebGL1.trim());
});

// reference: https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html
test('built-in shaders should be written in WebGL2', () => {
const shaderFiles = globSync('../../src/shaders/*.glsl');
for (const shaderFile of shaderFiles) {
const shaderSource = fs.readFileSync(shaderFile, 'utf8');
expect(shaderSource.includes('attribute')).toBe(false);
expect(shaderSource.includes('varying')).toBe(false);
expect(shaderSource.includes('gl_FragColor')).toBe(false);
expect(shaderSource.includes('texture2D')).toBe(false);
}
});
});

0 comments on commit 5ad1709

Please sign in to comment.