diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c1593cd2..0f93a732ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/build/generate-shaders.ts b/build/generate-shaders.ts index 5f3e2409d7..9e652dbe4d 100644 --- a/build/generate-shaders.ts +++ b/build/generate-shaders.ts @@ -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 @@ -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`); diff --git a/src/render/program.ts b/src/render/program.ts index 78110d0344..efdbe51c45 100644 --- a/src/render/program.ts +++ b/src/render/program.ts @@ -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'; @@ -72,6 +73,9 @@ export class Program { } const defines = configuration ? configuration.defines() : []; + if (isWebGL2(gl)) { + defines.unshift('#version 300 es'); + } if (showOverdrawInspector) { defines.push('#define OVERDRAW_INSPECTOR;'); } @@ -82,8 +86,13 @@ export class Program { 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()) { diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index bc7b88f2ec..d8ad62f0d3 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -1,4 +1,3 @@ - // Disable Flow annotations here because Flow doesn't support importing GLSL files import preludeFrag from './_prelude.fragment.glsl.g'; @@ -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 = {}; @@ -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 @@ -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 @@ -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 @@ -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('); } diff --git a/test/build/shaders.test.ts b/test/build/shaders.test.ts new file mode 100644 index 0000000000..3bf16ff5ae --- /dev/null +++ b/test/build/shaders.test.ts @@ -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); + } + }); +});