/** * Class for generating shaders from literal style objects * @module ol/render/webgl/ShaderBuilder */ import {colorToGlsl, numberToGlsl, stringToGlsl} from '../../expr/gpu.js'; import {createDefaultStyle} from '../../style/flat.js'; import {LINESTRING_ANGLE_COSINE_CUTOFF} from './bufferUtil.js'; import {UNPACK_COLOR_FN} from './compileUtil.js'; export const COMMON_HEADER = `#ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif uniform mat4 u_projectionMatrix; uniform mat4 u_screenToWorldMatrix; uniform vec2 u_viewportSizePx; uniform float u_pixelRatio; uniform float u_globalAlpha; uniform float u_time; uniform float u_zoom; uniform float u_resolution; uniform float u_rotation; uniform vec4 u_renderExtent; uniform vec2 u_patternOrigin; uniform float u_depth; uniform mediump int u_hitDetection; const float PI = 3.141592653589793238; const float TWO_PI = 2.0 * PI; float currentLineMetric = 0.; // an actual value will be used in the stroke shaders ${UNPACK_COLOR_FN} `; const DEFAULT_STYLE = createDefaultStyle(); /** * @typedef {Object} AttributeDescription * @property {string} name Attribute name, as will be declared in the header of the vertex shader (including a_) * @property {string} type Attribute GLSL type, either `float`, `vec2`, `vec4`... * @property {string} varyingName Varying name, as will be declared in the header of both shaders (including v_) * @property {string} varyingType Varying type, either `float`, `vec2`, `vec4`... * @property {string} varyingExpression GLSL expression to assign to the varying in the vertex shader (e.g. `unpackColor(a_myAttr)`) */ /** * @typedef {Object} UniformDescription * @property {string} name Uniform name, as will be declared in the header of the vertex shader (including u_) * @property {string} type Uniform GLSL type, either `float`, `vec2`, `vec4`... */ /** * @classdesc * This class implements a classic builder pattern for generating many different types of shaders. * Methods can be chained, e. g.: * * ```js * const shader = new ShaderBuilder() * .addAttribute('a_width', 'float') * .addUniform('u_time', 'float) * .setColorExpression('...') * .setSymbolSizeExpression('...') * .getSymbolFragmentShader(); * ``` * * A note on [alpha premultiplication](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied): * The ShaderBuilder class expects all colors to **not having been alpha-premultiplied!** This is because alpha * premultiplication is done at the end of each fragment shader. */ export class ShaderBuilder { constructor() { /** * Uniforms; these will be declared in the header (should include the type). * @type {Array} * @private */ this.uniforms_ = []; /** * Attributes; these will be declared in the header (should include the type). * @type {Array} * @private */ this.attributes_ = []; /** * @type {boolean} * @private */ this.hasSymbol_ = false; /** * @type {string} * @private */ this.symbolSizeExpression_ = `vec2(${numberToGlsl( DEFAULT_STYLE['circle-radius'], )} + ${numberToGlsl(DEFAULT_STYLE['circle-stroke-width'] * 0.5)})`; /** * @type {string} * @private */ this.symbolRotationExpression_ = '0.0'; /** * @type {string} * @private */ this.symbolOffsetExpression_ = 'vec2(0.0)'; /** * @type {string} * @private */ this.symbolColorExpression_ = colorToGlsl( /** @type {string} */ (DEFAULT_STYLE['circle-fill-color']), ); /** * @type {string} * @private */ this.texCoordExpression_ = 'vec4(0.0, 0.0, 1.0, 1.0)'; /** * @type {string} * @private */ this.discardExpression_ = 'false'; /** * @type {boolean} * @private */ this.symbolRotateWithView_ = false; /** * @type {boolean} * @private */ this.hasStroke_ = false; /** * @type {string} * @private */ this.strokeWidthExpression_ = numberToGlsl(DEFAULT_STYLE['stroke-width']); /** * @type {string} * @private */ this.strokeColorExpression_ = colorToGlsl( /** @type {string} */ (DEFAULT_STYLE['stroke-color']), ); /** * @private */ this.strokeOffsetExpression_ = '0.'; /** * @private */ this.strokeCapExpression_ = stringToGlsl('round'); /** * @private */ this.strokeJoinExpression_ = stringToGlsl('round'); /** * @private */ this.strokeMiterLimitExpression_ = '10.'; /** * @private */ this.strokeDistanceFieldExpression_ = '-1000.'; /** * @private * @type {string} */ this.strokePatternLengthExpression_ = null; /** * @type {boolean} * @private */ this.hasFill_ = false; /** * @type {string} * @private */ this.fillColorExpression_ = colorToGlsl( /** @type {string} */ (DEFAULT_STYLE['fill-color']), ); /** * @type {Array} * @private */ this.vertexShaderFunctions_ = []; /** * @type {Array} * @private */ this.fragmentShaderFunctions_ = []; } /** * Adds a uniform accessible in both fragment and vertex shaders. * The given name should include a type, such as `sampler2D u_texture`. * @param {string} name Uniform name, including the `u_` prefix * @param {'float'|'vec2'|'vec3'|'vec4'|'sampler2D'} type GLSL type * @return {ShaderBuilder} the builder object */ addUniform(name, type) { this.uniforms_.push({ name, type, }); return this; } /** * Adds an attribute accessible in the vertex shader, read from the geometry buffer. * The given name should include a type, such as `vec2 a_position`. * Attributes will also be made available under the same name in fragment shaders. * @param {string} name Attribute name, including the `a_` prefix * @param {'float'|'vec2'|'vec3'|'vec4'} type GLSL type * @param {string} [varyingExpression] Expression which will be assigned to the varying in the vertex shader, and * passed on to the fragment shader. * @param {'float'|'vec2'|'vec3'|'vec4'} [varyingType] Type of the attribute after transformation; * e.g. `vec4` after unpacking color components * @return {ShaderBuilder} the builder object */ addAttribute(name, type, varyingExpression, varyingType) { this.attributes_.push({ name, type, varyingName: name.replace(/^a_/, 'v_'), varyingType: varyingType ?? type, varyingExpression: varyingExpression ?? name, }); return this; } /** * Sets an expression to compute the size of the shape. * This expression can use all the uniforms and attributes available * in the vertex shader, and should evaluate to a `vec2` value. * @param {string} expression Size expression * @return {ShaderBuilder} the builder object */ setSymbolSizeExpression(expression) { this.hasSymbol_ = true; this.symbolSizeExpression_ = expression; return this; } /** * @return {string} The current symbol size expression */ getSymbolSizeExpression() { return this.symbolSizeExpression_; } /** * Sets an expression to compute the rotation of the shape. * This expression can use all the uniforms and attributes available * in the vertex shader, and should evaluate to a `float` value in radians. * @param {string} expression Size expression * @return {ShaderBuilder} the builder object */ setSymbolRotationExpression(expression) { this.symbolRotationExpression_ = expression; return this; } /** * Sets an expression to compute the offset of the symbol from the point center. * This expression can use all the uniforms and attributes available * in the vertex shader, and should evaluate to a `vec2` value. * @param {string} expression Offset expression * @return {ShaderBuilder} the builder object */ setSymbolOffsetExpression(expression) { this.symbolOffsetExpression_ = expression; return this; } /** * @return {string} The current symbol offset expression */ getSymbolOffsetExpression() { return this.symbolOffsetExpression_; } /** * Sets an expression to compute the color of the shape. * This expression can use all the uniforms, varyings and attributes available * in the fragment shader, and should evaluate to a `vec4` value. * @param {string} expression Color expression * @return {ShaderBuilder} the builder object */ setSymbolColorExpression(expression) { this.hasSymbol_ = true; this.symbolColorExpression_ = expression; return this; } /** * @return {string} The current symbol color expression */ getSymbolColorExpression() { return this.symbolColorExpression_; } /** * Sets an expression to compute the texture coordinates of the vertices. * This expression can use all the uniforms and attributes available * in the vertex shader, and should evaluate to a `vec4` value. * @param {string} expression Texture coordinate expression * @return {ShaderBuilder} the builder object */ setTextureCoordinateExpression(expression) { this.texCoordExpression_ = expression; return this; } /** * Sets an expression to determine whether a fragment (pixel) should be discarded, * i.e. not drawn at all. * This expression can use all the uniforms, varyings and attributes available * in the fragment shader, and should evaluate to a `bool` value (it will be * used in an `if` statement) * @param {string} expression Fragment discard expression * @return {ShaderBuilder} the builder object */ setFragmentDiscardExpression(expression) { this.discardExpression_ = expression; return this; } /** * @return {string} The current fragment discard expression */ getFragmentDiscardExpression() { return this.discardExpression_; } /** * Sets whether the symbols should rotate with the view or stay aligned with the map. * Note: will only be used for point geometry shaders. * @param {boolean} rotateWithView Rotate with view * @return {ShaderBuilder} the builder object */ setSymbolRotateWithView(rotateWithView) { this.symbolRotateWithView_ = rotateWithView; return this; } /** * @param {string} expression Stroke width expression, returning value in pixels * @return {ShaderBuilder} the builder object */ setStrokeWidthExpression(expression) { this.hasStroke_ = true; this.strokeWidthExpression_ = expression; return this; } /** * @param {string} expression Stroke color expression, evaluate to `vec4`: can rely on currentLengthPx and currentRadiusPx * @return {ShaderBuilder} the builder object */ setStrokeColorExpression(expression) { this.hasStroke_ = true; this.strokeColorExpression_ = expression; return this; } /** * @return {string} The current stroke color expression */ getStrokeColorExpression() { return this.strokeColorExpression_; } /** * @param {string} expression Stroke color expression, evaluate to `float` * @return {ShaderBuilder} the builder object */ setStrokeOffsetExpression(expression) { this.strokeOffsetExpression_ = expression; return this; } /** * @param {string} expression Stroke line cap expression, evaluate to `float` * @return {ShaderBuilder} the builder object */ setStrokeCapExpression(expression) { this.strokeCapExpression_ = expression; return this; } /** * @param {string} expression Stroke line join expression, evaluate to `float` * @return {ShaderBuilder} the builder object */ setStrokeJoinExpression(expression) { this.strokeJoinExpression_ = expression; return this; } /** * @param {string} expression Stroke miter limit expression, evaluate to `float` * @return {ShaderBuilder} the builder object */ setStrokeMiterLimitExpression(expression) { this.strokeMiterLimitExpression_ = expression; return this; } /** * @param {string} expression Stroke distance field expression, evaluate to `float` * This can override the default distance field; can rely on currentLengthPx and currentRadiusPx * @return {ShaderBuilder} the builder object */ setStrokeDistanceFieldExpression(expression) { this.strokeDistanceFieldExpression_ = expression; return this; } /** * Defining a pattern length for a stroke lets us avoid having visual artifacts when * a linestring is very long and thus has very high "distance" attributes on its vertices. * If we apply a pattern or dash array to a stroke we know for certain that the full distance value * is not necessary and can be trimmed down using `mod(currentDistance, patternLength)`. * @param {string} expression Stroke expression that evaluates to a`float; value is expected to be * in pixels. * @return {ShaderBuilder} the builder object */ setStrokePatternLengthExpression(expression) { this.strokePatternLengthExpression_ = expression; return this; } /** * @return {string} The current stroke pattern length expression. */ getStrokePatternLengthExpression() { return this.strokePatternLengthExpression_; } /** * @param {string} expression Fill color expression, evaluate to `vec4` * @return {ShaderBuilder} the builder object */ setFillColorExpression(expression) { this.hasFill_ = true; this.fillColorExpression_ = expression; return this; } /** * @return {string} The current fill color expression */ getFillColorExpression() { return this.fillColorExpression_; } addVertexShaderFunction(code) { if (this.vertexShaderFunctions_.includes(code)) { return this; } this.vertexShaderFunctions_.push(code); return this; } addFragmentShaderFunction(code) { if (this.fragmentShaderFunctions_.includes(code)) { return this; } this.fragmentShaderFunctions_.push(code); return this; } /** * Generates a symbol vertex shader from the builder parameters * @return {string|null} The full shader as a string; null if no size or color specified */ getSymbolVertexShader() { if (!this.hasSymbol_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} attribute vec2 a_position; attribute vec2 a_localPosition; attribute vec2 a_hitColor; varying vec2 v_texCoord; varying vec2 v_quadCoord; varying vec4 v_hitColor; varying vec2 v_centerPx; varying float v_angle; varying vec2 v_quadSizePx; ${this.attributes_ .map( (attribute) => `attribute ${attribute.type} ${attribute.name}; varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.vertexShaderFunctions_.join('\n')} vec2 pxToScreen(vec2 coordPx) { vec2 scaled = coordPx / u_viewportSizePx / 0.5; return scaled; } vec2 screenToPx(vec2 coordScreen) { return (coordScreen * 0.5 + 0.5) * u_viewportSizePx; } void main(void) { v_quadSizePx = ${this.symbolSizeExpression_}; vec2 halfSizePx = v_quadSizePx * 0.5; vec2 centerOffsetPx = ${this.symbolOffsetExpression_}; vec2 offsetPx = centerOffsetPx + a_localPosition * halfSizePx * vec2(1., -1.); float angle = ${this.symbolRotationExpression_}${this.symbolRotateWithView_ ? ' + u_rotation' : ''}; float c = cos(-angle); float s = sin(-angle); offsetPx = vec2(c * offsetPx.x - s * offsetPx.y, s * offsetPx.x + c * offsetPx.y); vec4 center = u_projectionMatrix * vec4(a_position, 0.0, 1.0); gl_Position = center + vec4(pxToScreen(offsetPx), u_depth, 0.); vec4 texCoord = ${this.texCoordExpression_}; float u = mix(texCoord.s, texCoord.p, a_localPosition.x * 0.5 + 0.5); float v = mix(texCoord.t, texCoord.q, a_localPosition.y * 0.5 + 0.5); v_texCoord = vec2(u, v); v_hitColor = unpackColor(a_hitColor); v_angle = angle; c = cos(-v_angle); s = sin(-v_angle); centerOffsetPx = vec2(c * centerOffsetPx.x - s * centerOffsetPx.y, s * centerOffsetPx.x + c * centerOffsetPx.y); v_centerPx = screenToPx(center.xy) + centerOffsetPx; ${this.attributes_ .map( (attribute) => ` ${attribute.varyingName} = ${attribute.varyingExpression};`, ) .join('\n')} }`; } /** * Generates a symbol fragment shader from the builder parameters * @return {string|null} The full shader as a string; null if no size or color specified */ getSymbolFragmentShader() { if (!this.hasSymbol_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} varying vec2 v_texCoord; varying vec4 v_hitColor; varying vec2 v_centerPx; varying float v_angle; varying vec2 v_quadSizePx; ${this.attributes_ .map( (attribute) => `varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.fragmentShaderFunctions_.join('\n')} void main(void) { ${this.attributes_ .map( (attribute) => ` ${attribute.varyingType} ${attribute.name} = ${attribute.varyingName}; // assign to original attribute name`, ) .join('\n')} if (${this.discardExpression_}) { discard; } vec2 coordsPx = gl_FragCoord.xy / u_pixelRatio - v_centerPx; // relative to center float c = cos(v_angle); float s = sin(v_angle); coordsPx = vec2(c * coordsPx.x - s * coordsPx.y, s * coordsPx.x + c * coordsPx.y); gl_FragColor = ${this.symbolColorExpression_}; gl_FragColor.rgb *= gl_FragColor.a; if (u_hitDetection > 0) { if (gl_FragColor.a < 0.05) { discard; }; gl_FragColor = v_hitColor; } }`; } /** * Generates a stroke vertex shader from the builder parameters * @return {string|null} The full shader as a string; null if no size or color specified */ getStrokeVertexShader() { if (!this.hasStroke_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} attribute vec2 a_segmentStart; attribute vec2 a_segmentEnd; attribute vec2 a_localPosition; attribute float a_measureStart; attribute float a_measureEnd; attribute float a_angleTangentSum; attribute float a_distanceLow; attribute float a_distanceHigh; attribute vec2 a_joinAngles; attribute vec2 a_hitColor; varying vec2 v_segmentStartPx; varying vec2 v_segmentEndPx; varying float v_angleStart; varying float v_angleEnd; varying float v_width; varying vec4 v_hitColor; varying float v_distancePx; varying float v_measureStart; varying float v_measureEnd; ${this.attributes_ .map( (attribute) => `attribute ${attribute.type} ${attribute.name}; varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.vertexShaderFunctions_.join('\n')} vec2 worldToPx(vec2 worldPos) { vec4 screenPos = u_projectionMatrix * vec4(worldPos, 0.0, 1.0); return (0.5 * screenPos.xy + 0.5) * u_viewportSizePx; } vec4 pxToScreen(vec2 pxPos) { vec2 screenPos = 2.0 * pxPos / u_viewportSizePx - 1.0; return vec4(screenPos, u_depth, 1.0); } bool isCap(float joinAngle) { return joinAngle < -0.1; } vec2 getJoinOffsetDirection(vec2 normalPx, float joinAngle) { float halfAngle = joinAngle / 2.0; float c = cos(halfAngle); float s = sin(halfAngle); vec2 angleBisectorNormal = vec2(s * normalPx.x + c * normalPx.y, -c * normalPx.x + s * normalPx.y); float length = 1.0 / s; return angleBisectorNormal * length; } vec2 getOffsetPoint(vec2 point, vec2 normal, float joinAngle, float offsetPx) { // if on a cap or the join angle is too high, offset the line along the segment normal if (cos(joinAngle) > 0.998 || isCap(joinAngle)) { return point - normal * offsetPx; } // offset is applied along the inverted normal (positive offset goes "right" relative to line direction) return point - getJoinOffsetDirection(normal, joinAngle) * offsetPx; } void main(void) { v_angleStart = a_joinAngles.x; v_angleEnd = a_joinAngles.y; float startEndRatio = a_localPosition.x * 0.5 + 0.5; currentLineMetric = mix(a_measureStart, a_measureEnd, startEndRatio); // we're reading the fractional part while keeping the sign (so -4.12 gives -0.12, 3.45 gives 0.45) float lineWidth = ${this.strokeWidthExpression_}; float lineOffsetPx = ${this.strokeOffsetExpression_}; // compute segment start/end in px with offset vec2 segmentStartPx = worldToPx(a_segmentStart); vec2 segmentEndPx = worldToPx(a_segmentEnd); vec2 tangentPx = normalize(segmentEndPx - segmentStartPx); vec2 normalPx = vec2(-tangentPx.y, tangentPx.x); segmentStartPx = getOffsetPoint(segmentStartPx, normalPx, v_angleStart, lineOffsetPx), segmentEndPx = getOffsetPoint(segmentEndPx, normalPx, v_angleEnd, lineOffsetPx); // compute current vertex position float normalDir = -1. * a_localPosition.y; float tangentDir = -1. * a_localPosition.x; float angle = mix(v_angleStart, v_angleEnd, startEndRatio); vec2 joinDirection; vec2 positionPx = mix(segmentStartPx, segmentEndPx, startEndRatio); // if angle is too high, do not make a proper join if (cos(angle) > ${LINESTRING_ANGLE_COSINE_CUTOFF} || isCap(angle)) { joinDirection = normalPx * normalDir - tangentPx * tangentDir; } else { joinDirection = getJoinOffsetDirection(normalPx * normalDir, angle); } positionPx = positionPx + joinDirection * (lineWidth * 0.5 + 1.); // adding 1 pixel for antialiasing gl_Position = pxToScreen(positionPx); v_segmentStartPx = segmentStartPx; v_segmentEndPx = segmentEndPx; v_width = lineWidth; v_hitColor = unpackColor(a_hitColor); v_distancePx = a_distanceLow / u_resolution - (lineOffsetPx * a_angleTangentSum); float distanceHighPx = a_distanceHigh / u_resolution; ${ this.strokePatternLengthExpression_ !== null ? `v_distancePx = mod(v_distancePx, ${this.strokePatternLengthExpression_}); distanceHighPx = mod(distanceHighPx, ${this.strokePatternLengthExpression_}); ` : '' }v_distancePx += distanceHighPx; v_measureStart = a_measureStart; v_measureEnd = a_measureEnd; ${this.attributes_ .map( (attribute) => ` ${attribute.varyingName} = ${attribute.varyingExpression};`, ) .join('\n')} }`; } /** * Generates a stroke fragment shader from the builder parameters * * @return {string|null} The full shader as a string; null if no size or color specified */ getStrokeFragmentShader() { if (!this.hasStroke_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} varying vec2 v_segmentStartPx; varying vec2 v_segmentEndPx; varying float v_angleStart; varying float v_angleEnd; varying float v_width; varying vec4 v_hitColor; varying float v_distancePx; varying float v_measureStart; varying float v_measureEnd; ${this.attributes_ .map( (attribute) => `varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.fragmentShaderFunctions_.join('\n')} vec2 pxToWorld(vec2 pxPos) { vec2 screenPos = 2.0 * pxPos / u_viewportSizePx - 1.0; return (u_screenToWorldMatrix * vec4(screenPos, 0.0, 1.0)).xy; } bool isCap(float joinAngle) { return joinAngle < -0.1; } float segmentDistanceField(vec2 point, vec2 start, vec2 end, float width) { vec2 tangent = normalize(end - start); vec2 normal = vec2(-tangent.y, tangent.x); vec2 startToPoint = point - start; return abs(dot(startToPoint, normal)) - width * 0.5; } float buttCapDistanceField(vec2 point, vec2 start, vec2 end) { vec2 startToPoint = point - start; vec2 tangent = normalize(end - start); return dot(startToPoint, -tangent); } float squareCapDistanceField(vec2 point, vec2 start, vec2 end, float width) { return buttCapDistanceField(point, start, end) - width * 0.5; } float roundCapDistanceField(vec2 point, vec2 start, vec2 end, float width) { float onSegment = max(0., 1000. * dot(point - start, end - start)); // this is very high when inside the segment return length(point - start) - width * 0.5 - onSegment; } float roundJoinDistanceField(vec2 point, vec2 start, vec2 end, float width) { return roundCapDistanceField(point, start, end, width); } float bevelJoinField(vec2 point, vec2 start, vec2 end, float width, float joinAngle) { vec2 startToPoint = point - start; vec2 tangent = normalize(end - start); float c = cos(joinAngle * 0.5); float s = sin(joinAngle * 0.5); float direction = -sign(sin(joinAngle)); vec2 bisector = vec2(c * tangent.x - s * tangent.y, s * tangent.x + c * tangent.y); float radius = width * 0.5 * s; return dot(startToPoint, bisector * direction) - radius; } float miterJoinDistanceField(vec2 point, vec2 start, vec2 end, float width, float joinAngle) { if (cos(joinAngle) > ${LINESTRING_ANGLE_COSINE_CUTOFF}) { // avoid risking a division by zero return bevelJoinField(point, start, end, width, joinAngle); } float miterLength = 1. / sin(joinAngle * 0.5); float miterLimit = ${this.strokeMiterLimitExpression_}; if (miterLength > miterLimit) { return bevelJoinField(point, start, end, width, joinAngle); } return -1000.; } float capDistanceField(vec2 point, vec2 start, vec2 end, float width, float capType) { if (capType == ${stringToGlsl('butt')}) { return buttCapDistanceField(point, start, end); } else if (capType == ${stringToGlsl('square')}) { return squareCapDistanceField(point, start, end, width); } return roundCapDistanceField(point, start, end, width); } float joinDistanceField(vec2 point, vec2 start, vec2 end, float width, float joinAngle, float joinType) { if (joinType == ${stringToGlsl('bevel')}) { return bevelJoinField(point, start, end, width, joinAngle); } else if (joinType == ${stringToGlsl('miter')}) { return miterJoinDistanceField(point, start, end, width, joinAngle); } return roundJoinDistanceField(point, start, end, width); } float computeSegmentPointDistance(vec2 point, vec2 start, vec2 end, float width, float joinAngle, float capType, float joinType) { if (isCap(joinAngle)) { return capDistanceField(point, start, end, width, capType); } return joinDistanceField(point, start, end, width, joinAngle, joinType); } float distanceFromSegment(vec2 point, vec2 start, vec2 end) { vec2 tangent = end - start; vec2 startToPoint = point - start; // inspire by capsule fn in https://iquilezles.org/articles/distfunctions/ float h = clamp(dot(startToPoint, tangent) / dot(tangent, tangent), 0.0, 1.0); return length(startToPoint - tangent * h); } void main(void) { ${this.attributes_ .map( (attribute) => ` ${attribute.varyingType} ${attribute.name} = ${attribute.varyingName}; // assign to original attribute name`, ) .join('\n')} vec2 currentPointPx = gl_FragCoord.xy / u_pixelRatio; #ifdef GL_FRAGMENT_PRECISION_HIGH vec2 worldPos = pxToWorld(currentPointPx); if ( abs(u_renderExtent[0] - u_renderExtent[2]) > 0.0 && ( worldPos[0] < u_renderExtent[0] || worldPos[1] < u_renderExtent[1] || worldPos[0] > u_renderExtent[2] || worldPos[1] > u_renderExtent[3] ) ) { discard; } #endif float segmentLengthPx = length(v_segmentEndPx - v_segmentStartPx); segmentLengthPx = max(segmentLengthPx, 1.17549429e-38); // avoid divide by zero vec2 segmentTangent = (v_segmentEndPx - v_segmentStartPx) / segmentLengthPx; vec2 segmentNormal = vec2(-segmentTangent.y, segmentTangent.x); vec2 startToPointPx = currentPointPx - v_segmentStartPx; float lengthToPointPx = max(0., min(dot(segmentTangent, startToPointPx), segmentLengthPx)); float currentLengthPx = lengthToPointPx + v_distancePx; float currentRadiusPx = distanceFromSegment(currentPointPx, v_segmentStartPx, v_segmentEndPx); float currentRadiusRatio = dot(segmentNormal, startToPointPx) * 2. / v_width; currentLineMetric = mix(v_measureStart, v_measureEnd, lengthToPointPx / segmentLengthPx); if (${this.discardExpression_}) { discard; } float capType = ${this.strokeCapExpression_}; float joinType = ${this.strokeJoinExpression_}; float segmentStartDistance = computeSegmentPointDistance(currentPointPx, v_segmentStartPx, v_segmentEndPx, v_width, v_angleStart, capType, joinType); float segmentEndDistance = computeSegmentPointDistance(currentPointPx, v_segmentEndPx, v_segmentStartPx, v_width, v_angleEnd, capType, joinType); float distanceField = max( segmentDistanceField(currentPointPx, v_segmentStartPx, v_segmentEndPx, v_width), max(segmentStartDistance, segmentEndDistance) ); distanceField = max(distanceField, ${this.strokeDistanceFieldExpression_}); vec4 color = ${this.strokeColorExpression_}; color.a *= smoothstep(0.5, -0.5, distanceField); gl_FragColor = color; gl_FragColor.a *= u_globalAlpha; gl_FragColor.rgb *= gl_FragColor.a; if (u_hitDetection > 0) { if (gl_FragColor.a < 0.1) { discard; }; gl_FragColor = v_hitColor; } }`; } /** * Generates a fill vertex shader from the builder parameters * * @return {string|null} The full shader as a string; null if no color specified */ getFillVertexShader() { if (!this.hasFill_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} attribute vec2 a_position; attribute vec2 a_hitColor; varying vec4 v_hitColor; ${this.attributes_ .map( (attribute) => `attribute ${attribute.type} ${attribute.name}; varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.vertexShaderFunctions_.join('\n')} void main(void) { gl_Position = u_projectionMatrix * vec4(a_position, u_depth, 1.0); v_hitColor = unpackColor(a_hitColor); ${this.attributes_ .map( (attribute) => ` ${attribute.varyingName} = ${attribute.varyingExpression};`, ) .join('\n')} }`; } /** * Generates a fill fragment shader from the builder parameters * @return {string|null} The full shader as a string; null if no color specified */ getFillFragmentShader() { if (!this.hasFill_) { return null; } return `${COMMON_HEADER} ${this.uniforms_.map((uniform) => `uniform ${uniform.type} ${uniform.name};`).join('\n')} varying vec4 v_hitColor; ${this.attributes_ .map( (attribute) => `varying ${attribute.varyingType} ${attribute.varyingName};`, ) .join('\n')} ${this.fragmentShaderFunctions_.join('\n')} vec2 pxToWorld(vec2 pxPos) { vec2 screenPos = 2.0 * pxPos / u_viewportSizePx - 1.0; return (u_screenToWorldMatrix * vec4(screenPos, 0.0, 1.0)).xy; } vec2 worldToPx(vec2 worldPos) { vec4 screenPos = u_projectionMatrix * vec4(worldPos, 0.0, 1.0); return (0.5 * screenPos.xy + 0.5) * u_viewportSizePx; } void main(void) { ${this.attributes_ .map( (attribute) => ` ${attribute.varyingType} ${attribute.name} = ${attribute.varyingName}; // assign to original attribute name`, ) .join('\n')} vec2 pxPos = gl_FragCoord.xy / u_pixelRatio; vec2 pxOrigin = worldToPx(u_patternOrigin); #ifdef GL_FRAGMENT_PRECISION_HIGH vec2 worldPos = pxToWorld(pxPos); if ( abs(u_renderExtent[0] - u_renderExtent[2]) > 0.0 && ( worldPos[0] < u_renderExtent[0] || worldPos[1] < u_renderExtent[1] || worldPos[0] > u_renderExtent[2] || worldPos[1] > u_renderExtent[3] ) ) { discard; } #endif if (${this.discardExpression_}) { discard; } gl_FragColor = ${this.fillColorExpression_}; gl_FragColor.a *= u_globalAlpha; gl_FragColor.rgb *= gl_FragColor.a; if (u_hitDetection > 0) { if (gl_FragColor.a < 0.1) { discard; }; gl_FragColor = v_hitColor; } }`; } }