diff --git a/packages/engine/Source/Scene/BufferPolylineMaterial.js b/packages/engine/Source/Scene/BufferPolylineMaterial.js index 354a40dd883..7c803b28247 100644 --- a/packages/engine/Source/Scene/BufferPolylineMaterial.js +++ b/packages/engine/Source/Scene/BufferPolylineMaterial.js @@ -1,9 +1,9 @@ // @ts-check +import Color from "../Core/Color.js"; import Frozen from "../Core/Frozen.js"; import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; -/** @import Color from "../Core/Color.js"; */ /** @import BufferPolyline from "./BufferPolyline.js"; */ /** @@ -12,6 +12,10 @@ import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; * @property {Color} [outlineColor=Color.WHITE] Color of outline. * @property {number} [outlineWidth=0.0] Width of outline, 0-255px. * @property {number} [width=1.0] Width of line, 0-255px. + * @property {Color} [gapColor=Color.TRANSPARENT] Color drawn in the gaps between dashes. + * @property {number} [dashRepeat=1] Number of dash-pattern cycles along the whole line, 0-255. The pattern is anchored to the line geometry (not the screen), so bands stay put as the camera pans and scale with the line as it zooms. + * @property {number} [dashOffset=0.0] Phase offset as a fraction of one cycle, 0-1. Shifts where the pattern starts relative to the origin of the line. + * @property {number} [dashPattern=65535] 16-bit on/off bitmask sampled within each cycle. 65535 (0xFFFF) is fully solid. */ /** @@ -29,7 +33,11 @@ class BufferPolylineMaterial extends BufferPrimitiveMaterial { static Layout = { ...BufferPrimitiveMaterial.Layout, WIDTH_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH, - __BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4, + GAP_COLOR_U32: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4, + DASH_REPEAT_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 8, + DASH_OFFSET_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 9, + DASH_PATTERN_U16: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 10, + __BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 12, }; /** @@ -49,6 +57,37 @@ class BufferPolylineMaterial extends BufferPrimitiveMaterial { * @type {number} */ this.width = options.width ?? 1; + + /** + * Color drawn in the gaps between dashes. Defaults to transparent, which + * discards, so gaps read as empty space. + * @type {Color} + */ + this.gapColor = Color.clone(options.gapColor ?? Color.TRANSPARENT); + + /** + * Number of dash-pattern cycles along the whole line, 0–255. The pattern is + * anchored to the line geometry, so bands stay put as the camera pans and + * scale with the line as it zooms (rather than crawling in screen space). + * @type {number} + */ + this.dashRepeat = options.dashRepeat ?? 1; + + /** + * Phase offset as a fraction of one cycle, 0–1. Shifts where the pattern + * starts relative to the origin of the line (e.g. 0.25 lands a solid slot + * on the origin instead of a gap). + * @type {number} + */ + this.dashOffset = options.dashOffset ?? 0; + + /** + * 16-bit on/off bitmask sampled within each cycle — each bit is one of 16 + * slots, 1 draws {@link BufferPolylineMaterial#color}, 0 draws + * {@link BufferPolylineMaterial#gapColor}. 65535 (0xFFFF) is fully solid. + * @type {number} + */ + this.dashPattern = options.dashPattern ?? 65535; } /** @@ -60,6 +99,22 @@ class BufferPolylineMaterial extends BufferPrimitiveMaterial { static pack(material, view, byteOffset) { super.pack(material, view, byteOffset); view.setUint8(this.Layout.WIDTH_U8 + byteOffset, material.width); + view.setUint32( + this.Layout.GAP_COLOR_U32 + byteOffset, + material.gapColor.toRgba(), + true, + ); + view.setUint8(this.Layout.DASH_REPEAT_U8 + byteOffset, material.dashRepeat); + // Offset is a [0,1) fraction stored in 256ths so 0.25 round-trips exactly. + view.setUint8( + this.Layout.DASH_OFFSET_U8 + byteOffset, + Math.round(material.dashOffset * 256), + ); + view.setUint16( + this.Layout.DASH_PATTERN_U16 + byteOffset, + material.dashPattern, + true, + ); } /** @@ -72,6 +127,17 @@ class BufferPolylineMaterial extends BufferPrimitiveMaterial { static unpack(view, byteOffset, result) { super.unpack(view, byteOffset, result); result.width = view.getUint8(this.Layout.WIDTH_U8 + byteOffset); + Color.fromRgba( + view.getUint32(this.Layout.GAP_COLOR_U32 + byteOffset, true), + result.gapColor, + ); + result.dashRepeat = view.getUint8(this.Layout.DASH_REPEAT_U8 + byteOffset); + result.dashOffset = + view.getUint8(this.Layout.DASH_OFFSET_U8 + byteOffset) / 256; + result.dashPattern = view.getUint16( + this.Layout.DASH_PATTERN_U16 + byteOffset, + true, + ); return result; } @@ -86,7 +152,14 @@ class BufferPolylineMaterial extends BufferPrimitiveMaterial { * @returns {Object} JSON-serializable object. */ toJSON() { - return { ...super.toJSON(), width: this.width }; + return { + ...super.toJSON(), + width: this.width, + gapColor: this.gapColor.toCssHexString(), + dashRepeat: this.dashRepeat, + dashOffset: this.dashOffset, + dashPattern: this.dashPattern, + }; } } diff --git a/packages/engine/Source/Scene/renderBufferPolylineCollection.js b/packages/engine/Source/Scene/renderBufferPolylineCollection.js index d7263dc12bc..0047ed97052 100644 --- a/packages/engine/Source/Scene/renderBufferPolylineCollection.js +++ b/packages/engine/Source/Scene/renderBufferPolylineCollection.js @@ -30,7 +30,7 @@ import BlendOption from "./BlendOption.js"; /** * TODO(PR#13211): Need 'keyof' syntax to avoid duplicating attribute names. - * @typedef {'positionHigh' | 'positionLow' | 'prevPositionHigh' | 'prevPositionLow' | 'nextPositionHigh' | 'nextPositionLow' | 'pickColor' | 'showColorWidthAndTexCoord' | 'alpha'} BufferPolylineAttribute + * @typedef {'positionHigh' | 'positionLow' | 'prevPositionHigh' | 'prevPositionLow' | 'nextPositionHigh' | 'nextPositionLow' | 'pickColor' | 'showColorWidthAndTexCoord' | 'alpha' | 'dashParams'} BufferPolylineAttribute * @ignore */ @@ -49,6 +49,7 @@ const BufferPolylineAttributeLocationsFloat64 = { pickColor: 6, showColorWidthAndTexCoord: 7, alpha: 8, + dashParams: 9, }; /** @@ -63,6 +64,7 @@ const BufferPolylineAttributeLocations = { pickColor: 3, showColorWidthAndTexCoord: 4, alpha: 5, + dashParams: 6, }; /** @@ -162,6 +164,8 @@ function renderBufferPolylineCollection(collection, frameState, renderContext) { pickColor: new Uint8Array(vertexCountMax * 4), showColorWidthAndTexCoord: new Float32Array(vertexCountMax * 4), alpha: new Uint8Array(vertexCountMax), + // (encodedGapColorRGB8, gapAlpha, dashRepeat + dashOffset, dashPattern) + dashParams: new Float32Array(vertexCountMax * 4), }; } @@ -175,6 +179,7 @@ function renderBufferPolylineCollection(collection, frameState, renderContext) { pickColor: pickColorArray, showColorWidthAndTexCoord: showColorWidthAndTexCoordArray, alpha: alphaArray, + dashParams: dashParamsArray, // 8-32 bit. position, @@ -200,6 +205,14 @@ function renderBufferPolylineCollection(collection, frameState, renderContext) { polyline.getMaterial(material); const encodedColor = AttributeCompression.encodeRGB8(material.color); const colorAlpha = material.color.alpha; + const encodedGapColor = AttributeCompression.encodeRGB8( + material.gapColor, + ); + const gapAlpha = material.gapColor.alpha; + // Pack the integer cycle count and the [0,1) phase offset into one float; + // the shader splits them with floor()/fract(). + const dashRepeatAndOffset = material.dashRepeat + material.dashOffset; + const dashPattern = material.dashPattern; Color.fromRgba(polyline._pickId, pickColor); const show = polyline.show; @@ -311,6 +324,11 @@ function renderBufferPolylineCollection(collection, frameState, renderContext) { alphaArray[vOffset] = colorAlpha * 255.0; + dashParamsArray[vOffset * 4] = encodedGapColor; + dashParamsArray[vOffset * 4 + 1] = gapAlpha; + dashParamsArray[vOffset * 4 + 2] = dashRepeatAndOffset; + dashParamsArray[vOffset * 4 + 3] = dashPattern; + vOffset++; } } @@ -462,6 +480,16 @@ function renderBufferPolylineCollection(collection, frameState, renderContext) { usage: BufferUsage.STATIC_DRAW, }), }, + { + index: attributeLocations.dashParams, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + vertexBuffer: Buffer.createVertexBuffer({ + typedArray: attributeArrays.dashParams, + context, + usage: BufferUsage.STATIC_DRAW, + }), + }, ], }); } else if (collection._dirtyCount > 0) { diff --git a/packages/engine/Source/Shaders/BufferPolylineMaterialFS.glsl b/packages/engine/Source/Shaders/BufferPolylineMaterialFS.glsl index 2bc998aab54..26ea2869b8e 100644 --- a/packages/engine/Source/Shaders/BufferPolylineMaterialFS.glsl +++ b/packages/engine/Source/Shaders/BufferPolylineMaterialFS.glsl @@ -1,13 +1,39 @@ in vec4 v_pickColor; in vec4 v_color; +in vec4 v_gapColor; +in float v_along; // 0..1 along the line, anchored to the geometry +in float v_dashRepeat; // integer part = cycles along the line, fractional part = phase offset +in float v_dashPattern; + +// Number of on/off slots in one dash cycle; matches the 16-bit v_dashPattern. +const float maskLength = 16.0; void main() { - if (v_color.a < 0.005) // matches 0/255 and 1/255 + vec4 fragColor = v_color; + + // Geometry-anchored dash: walk `dashRepeat` pattern cycles along the line + // (v_along, 0..1) and read the matching bit out of the 16-bit v_dashPattern. + // Sampling along the geometry (rather than gl_FragCoord) keeps the bands + // fixed to the line as the camera pans and scales them with the line as it + // zooms. `dashOffset` phase-shifts the pattern (e.g. 0.25 lands a solid slot + // on the origin). A solid line uses an all-ones pattern (0xFFFF), so this + // never selects the gap. + float dashRepeat = floor(v_dashRepeat); + float dashOffset = fract(v_dashRepeat); + float dashPosition = fract(v_along * dashRepeat + dashOffset); + float maskIndex = floor(dashPosition * maskLength); + float maskTest = floor(v_dashPattern / pow(2.0, maskIndex)); + if (mod(maskTest, 2.0) < 1.0) + { + fragColor = v_gapColor; + } + + if (fragColor.a < 0.005) // matches 0/255 and 1/255 { discard; } - out_FragColor = czm_gammaCorrect(v_color); + out_FragColor = czm_gammaCorrect(fragColor); czm_writeLogDepth(); } diff --git a/packages/engine/Source/Shaders/BufferPolylineMaterialVS.glsl b/packages/engine/Source/Shaders/BufferPolylineMaterialVS.glsl index c6257ee8c3c..afd3e1da1cd 100644 --- a/packages/engine/Source/Shaders/BufferPolylineMaterialVS.glsl +++ b/packages/engine/Source/Shaders/BufferPolylineMaterialVS.glsl @@ -13,12 +13,17 @@ in vec3 nextPosition; in vec4 pickColor; in vec4 showColorWidthAndTexCoord; in float alpha; +in vec4 dashParams; // (encodedGapColorRGB8, gapAlpha, dashRepeat + dashOffset, dashPattern) out vec4 v_pickColor; out vec4 v_color; out vec2 v_st; out float v_width; out float v_polylineAngle; +out vec4 v_gapColor; +out float v_along; +out float v_dashRepeat; +out float v_dashPattern; void main() { @@ -60,4 +65,10 @@ void main() v_width = width; v_polylineAngle = polylineAngle; + + v_gapColor = czm_decodeRGB8(dashParams.x); + v_gapColor.a = dashParams.y; + v_along = texCoord; + v_dashRepeat = dashParams.z; // integer part = cycles, fractional part = phase offset + v_dashPattern = dashParams.w; }