From 194ec5101506f9253177c2da1f892aab7c1e0401 Mon Sep 17 00:00:00 2001 From: LeonidPolukhin <117165489+LeonidPolukhin@users.noreply.github.com> Date: Tue, 5 Dec 2023 20:51:59 +0300 Subject: [PATCH] feat: Well Markers Layer (#1818) The layer can display markers of three flat shapes: circle, square and triangle. For every marker the following attributes can be defined: - center position in 3D. - azimuth, rotation angle relative to north direction. - inclination, rotation angle relative to vertical direction. - size. - fill color with transparency. - outline color. --------- Co-authored-by: leonid.polukhin --- .../subsurface-viewer/src/layers/index.ts | 2 + .../src/layers/well_markers/fragment.glsl.ts | 13 + .../src/layers/well_markers/vertex.glsl.ts | 55 +++ .../well_markers/wellMarkersLayer.stories.tsx | 107 +++++ .../layers/well_markers/wellMarkersLayer.ts | 431 ++++++++++++++++++ 5 files changed, 608 insertions(+) create mode 100644 typescript/packages/subsurface-viewer/src/layers/well_markers/fragment.glsl.ts create mode 100644 typescript/packages/subsurface-viewer/src/layers/well_markers/vertex.glsl.ts create mode 100644 typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.stories.tsx create mode 100644 typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.ts diff --git a/typescript/packages/subsurface-viewer/src/layers/index.ts b/typescript/packages/subsurface-viewer/src/layers/index.ts index ae33f4652..c09843d8b 100644 --- a/typescript/packages/subsurface-viewer/src/layers/index.ts +++ b/typescript/packages/subsurface-viewer/src/layers/index.ts @@ -7,6 +7,7 @@ export { default as Map3DLayer } from "./terrain/map3DLayer"; export { default as DrawingLayer } from "./drawing/drawingLayer"; export { default as Hillshading2DLayer } from "./hillshading2d/hillshading2dLayer"; export { default as WellsLayer } from "./wells/wellsLayer"; +export { default as WellMarkersLayer } from "./well_markers/wellMarkersLayer"; export { default as PieChartLayer } from "./piechart/pieChartLayer"; export { default as FaultPolygonsLayer } from "./fault_polygons/faultPolygonsLayer"; export { default as AxesLayer } from "./axes/axesLayer"; @@ -31,5 +32,6 @@ export type { NorthArrow3DLayerProps } from "./northarrow/northArrow3DLayer"; export type { PieChartLayerProps } from "./piechart/pieChartLayer"; export type { Map3DLayerProps } from "./terrain/map3DLayer"; export type { WellsLayerProps } from "./wells/wellsLayer"; +export { default as WellMarkersLayerProps } from "./well_markers/wellMarkersLayer"; export type { Grid3DLayerProps } from "./grid3d/grid3dLayer"; export type { BoxSelectionLayerProps } from "./BoxSelectionLayer/boxSelectionLayer"; diff --git a/typescript/packages/subsurface-viewer/src/layers/well_markers/fragment.glsl.ts b/typescript/packages/subsurface-viewer/src/layers/well_markers/fragment.glsl.ts new file mode 100644 index 000000000..f5e5ad54c --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/layers/well_markers/fragment.glsl.ts @@ -0,0 +1,13 @@ +export const fsShader = `#version 300 es +#define SHADER_NAME well-markers-fragment-shader + +precision highp float; + +in vec4 color; + +void main(void) { + + gl_FragColor = vec4(color.rgba * (1.0 / 255.0)); + DECKGL_FILTER_COLOR(gl_FragColor, geometry); +} +`; diff --git a/typescript/packages/subsurface-viewer/src/layers/well_markers/vertex.glsl.ts b/typescript/packages/subsurface-viewer/src/layers/well_markers/vertex.glsl.ts new file mode 100644 index 000000000..2b6b010bb --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/layers/well_markers/vertex.glsl.ts @@ -0,0 +1,55 @@ +export const vsShader = `#version 300 es +#define SHADER_NAME well-markers-vertex-shader +precision highp float; + +attribute vec3 positions; +attribute vec3 instancePositions; +attribute float instanceSizes; +attribute float instanceAzimuths; +attribute float instanceInclinations; +attribute vec4 instanceColors; +attribute vec4 instanceOutlineColors; + +attribute vec3 instancePickingColors; + +uniform int sizeUnits; +uniform bool ZIncreasingDownwards; +uniform bool useOutlineColor; + + +out vec4 position_commonspace; +out vec4 color; + +void main(void) { + + vec3 position = instancePositions; + position.z *= (ZIncreasingDownwards? -1.0 : 1.0); + + geometry.worldPosition = position; + geometry.pickingColor = instancePickingColors; + + color = useOutlineColor ? instanceOutlineColors : instanceColors; + + float sizeInPixels = project_size_to_pixel(instanceSizes, sizeUnits); + float projectedSize = project_pixel_size(sizeInPixels); + + float sinA = sin (PI / 180.0 * instanceAzimuths); + float cosA = cos (PI / 180.0 * instanceAzimuths); + + float sinI = sin (PI / 180.0 * instanceInclinations); + float cosI = cos (PI / 180.0 * instanceInclinations); + + mat3 azimuthMatrix = mat3(vec3(cosA, sinA, 0.0), vec3(-sinA, cosA, 0.0), vec3(0.0, 0.0, 1.0)); + mat3 inclMatrix = mat3(vec3(1.0, 0.0, 0.0), vec3(0.0, cosI, sinI), vec3(0.0, -sinI, cosI)); + mat3 sizeMatrix = mat3(vec3(projectedSize, 0.0, 0.0), vec3(0.0, projectedSize, 0.0), vec3(0.0, 0.0, 1.0)); + vec3 rotatedPos = azimuthMatrix * inclMatrix * sizeMatrix *positions; + + position_commonspace = vec4(project_position(rotatedPos + position), 0.0); + gl_Position = project_common_position_to_clipspace(position_commonspace); + + vec4 dummyColor = vec4(0.0); + + DECKGL_FILTER_GL_POSITION(gl_Position, geometry); + DECKGL_FILTER_COLOR(dummyColor, geometry); +} +`; diff --git a/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.stories.tsx b/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.stories.tsx new file mode 100644 index 000000000..5454f9f6e --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.stories.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { create, all } from "mathjs"; + +import type { StoryFn, Meta } from "@storybook/react"; +import SubsurfaceViewer from "../../SubsurfaceViewer"; + +import type { WellMarkerDataT } from "./wellMarkersLayer"; + +export default { + component: SubsurfaceViewer, + title: "SubsurfaceViewer/Well Markers Layer", +} as Meta; + +const Template: StoryFn = (args) => ( + +); + +const parameters = { + docs: { + description: { + story: "Well Markers Layer.", + }, + inlineStories: false, + iframeHeight: 500, + }, +}; + +const math = create(all, { randomSeed: "1984" }); + +type TRandomNumberFunc = (max: number) => number; + +const randomFunc = ((): TRandomNumberFunc => { + if (math?.random) { + return (max: number) => { + const val = math.random?.(max); + return val ? val : 0.0; + }; + } + return (max: number) => Math.random() * max; +})(); + +const generateMarkers = (): WellMarkerDataT[] => { + const N = 40; + const M = 40; + + const dN = (2 * Math.PI) / N; + const dM = (5 * Math.PI) / M; + + const res: WellMarkerDataT[] = []; + + for (let i = 0; i < N; ++i) { + for (let j = 0; j < M; ++j) { + const x = -N / 2 + i; + const y = -M / 2 + j; + const az = dN * i; + const incl = dM * j; + + const z = 5 * (Math.sin(incl) * Math.cos(az)); + res.push({ + position: [x, y, z], + azimuth: (az * 180.0) / Math.PI, + inclination: (Math.asin(Math.cos(incl)) * 180.0) / Math.PI, + color: [randomFunc(255), randomFunc(255), randomFunc(255), 100], + outlineColor: [0, 0, 100, 255], + size: 0.02 * Math.sqrt(x * x + y * y), + }); + } + } + return res; +}; + +export const WellMarkers = Template.bind({}); + +WellMarkers.args = { + bounds: [-25, -25, 50, 30], + views: { + layout: [1, 1] as [number, number], + viewports: [ + { + id: "view_1", + show3D: true, + }, + ], + }, + id: "well-markers-tttt", + layers: [ + { + "@@type": "AxesLayer", + id: "well-markers-axes", + bounds: [-25, -25, -25, 25, 25, 25], + ZIncreasingDownwards: false, + }, + { + "@@type": "NorthArrow3DLayer", + id: "north-arrow-layer", + }, + { + "@@type": "WellMarkersLayer", + id: "well-markers-1", + pickable: true, + shape: "circle", + sizeUnits: "common", + data: generateMarkers(), + }, + ], +}; +WellMarkers.parameters = parameters; diff --git a/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.ts b/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.ts new file mode 100644 index 000000000..5f94f695e --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/layers/well_markers/wellMarkersLayer.ts @@ -0,0 +1,431 @@ +import GL from "@luma.gl/constants"; +import { Geometry, Model } from "@luma.gl/engine"; +import type { + Accessor, + Color, + DefaultProps, + LayerContext, + Position, + PickingInfo, + UpdateParameters, + LayerProps, + Unit, +} from "@deck.gl/core/typed"; +import { Layer, project, picking, UNIT } from "@deck.gl/core/typed"; + +import type { + ExtendedLayerProps, + LayerPickInfo, + PropertyDataType, +} from "../utils/layerTools"; +import { createPropertyData } from "../utils/layerTools"; +import { utilities } from "../shader_modules"; + +import { vsShader } from "./vertex.glsl"; +import { fsShader } from "./fragment.glsl"; + +export type WellMarkersLayerProps = _WellMarkersLayerProps & LayerProps; + +/** + * Input data of the layer. + */ +export type WellMarkerDataT = { + /** + * Position of a marker center. + */ + position: Position; + + /** + * Size of a marker in size units. + */ + size: number; + /** + * Azimuth of the a marker in degrees. + */ + azimuth: number; + /** + * Inclination of a marker against vertical direcion in degrees. + */ + inclination: number; + /** + * Fill color of a marker. + */ + color: Color; + /** + * Outline color of a marker. + */ + outlineColor: Color; +}; +export interface _WellMarkersLayerProps extends ExtendedLayerProps { + /** + * Shape of the markers. + * @default 'circle' + */ + shape: "triangle" | "circle" | "square"; + /** + * The units of the marker size, one of `'meters'`, `'common'`, and `'pixels'`. + * @default 'meters' + */ + sizeUnits: Unit; + /** If true means that input z values are interpreted as depths. + * For example depth of z = 1000 corresponds to -1000 on the z axis. + * @default 'true' + */ + ZIncreasingDownwards: boolean; + + /** + * Center position accessor. + */ + getPosition?: Accessor; + /** + * Size accessor. + */ + getSize?: Accessor; + /** + * Azimuth accessor. + */ + getAzimuth?: Accessor; + /** + * Inclination accessor. + */ + getInclination?: Accessor; + /** + * Color accessor. + */ + getColor?: Accessor; + /** + * Outline color accessor. + */ + getOutlineColor?: Accessor; +} + +const normalizeColor = (color: Color | undefined): Color => { + if (!color) { + return new Uint8Array([0, 0, 0, 255]); + } + + if (color.length > 4) { + return new Uint8Array(color.slice(0, 4)); + } + + switch (color.length) { + case 0: + return new Uint8Array([0, 0, 0, 255]); + case 1: + return new Uint8Array([...color, 0, 0, 255]); + case 2: + return new Uint8Array([...color, 0, 255]); + case 3: + return new Uint8Array([...color, 255]); + default: + return color; + } +}; + +const defaultProps: DefaultProps = { + "@@type": "WellMarkersLayer", + name: "Well Markers", + id: "well-markers", + shape: "circle", + sizeUnits: "meters", + visible: true, + ZIncreasingDownwards: true, + getPosition: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return x.position; + }, + }, + getSize: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return x.size; + }, + }, + getAzimuth: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return x.azimuth; + }, + }, + getInclination: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return x.inclination; + }, + }, + getColor: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return normalizeColor(x.color); + }, + }, + getOutlineColor: { + type: "accessor", + value: (x: WellMarkerDataT) => { + return normalizeColor(x.outlineColor); + }, + }, +}; + +interface IMarkerShape { + positions: Float32Array; + outline: Float32Array; + drawMode: number; +} + +export default class WellMarkersLayer extends Layer { + private shapes: Map = new Map(); + + constructor(props: WellMarkersLayerProps) { + super(props); + this.initShapes(); + } + + initializeState(): void { + this.getAttributeManager()!.addInstanced({ + instancePositions: { + size: 3, + type: GL.DOUBLE, + transition: true, + accessor: "getPosition", + }, + instanceSizes: { + size: 1, + type: GL.DOUBLE, + transition: true, + accessor: "getSize", + defaultValue: 1.0, + }, + instanceAzimuths: { + size: 1, + type: GL.DOUBLE, + transition: true, + accessor: "getAzimuth", + defaultValue: 0, + }, + instanceInclinations: { + size: 1, + type: GL.DOUBLE, + transition: true, + accessor: "getInclination", + defaultValue: 0, + }, + instanceColors: { + size: 4, + type: GL.UNSIGNED_BYTE, + transition: true, + accessor: "getColor", + defaultValue: [255, 0, 0, 255], + }, + instanceOutlineColors: { + size: 4, + type: GL.UNSIGNED_BYTE, + transition: true, + accessor: "getOutlineColor", + defaultValue: [255, 0, 255, 255], + }, + }); + const models = this._createModels(); + this.setState({ shapeModel: models[0], outlineModel: models[1] }); + } + + updateState(params: UpdateParameters) { + super.updateState(params); + + if ( + params.changeFlags.extensionsChanged || + params.changeFlags.propsChanged + ) { + this.state?.["shapeModel"]?.delete(); + this.state?.["outlineModel"]?.delete(); + const models = this._createModels(); + this.setState({ + ...this.state, + shapeModel: models[0], + outlineModel: models[1], + }); + this.getAttributeManager()!.invalidateAll(); + } + } + + getModels(): Model[] { + if (this.state["shapeModel"] && this.state["outlineModel"]) { + return [this.state["shapeModel"], this.state["outlineModel"]]; + } + return []; + } + + draw(args: { + moduleParameters?: unknown; + uniforms: number[]; + context: LayerContext; + }): void { + if (!this.state["shapeModel"]) { + return; + } + const { uniforms } = args; + const models = this.getModels(); + if (models.length && models.length < 2) { + return; + } + models[0] + .setUniforms({ + ...uniforms, + sizeUnits: UNIT[this.props.sizeUnits], + ZIncreasingDownwards: this.props.ZIncreasingDownwards, + }) + .draw(); + models[1] + .setUniforms({ + ...uniforms, + ZIncreasingDownwards: this.props.ZIncreasingDownwards, + sizeUnits: UNIT[this.props.sizeUnits], + }) + .draw(); + } + + getPickingInfo({ info }: { info: PickingInfo }): LayerPickInfo { + if (!info.color) { + return info; + } + + const layer_properties: PropertyDataType[] = []; + + const markerIndex = this.decodePickingColor(info.color); + const markerData = this.props.data as WellMarkerDataT[]; + + if (markerIndex >= 0 && markerIndex < markerData.length) { + layer_properties.push( + createPropertyData("Azimuth", markerData[markerIndex].azimuth) + ); + layer_properties.push( + createPropertyData( + "Inclination", + markerData[markerIndex].inclination + ) + ); + } + + if (typeof info.coordinate?.[2] !== "undefined") { + let depth = info.coordinate[2]; + depth = this.props.ZIncreasingDownwards ? -depth : depth; + layer_properties.push(createPropertyData("Depth", depth)); + } + return { + ...info, + properties: layer_properties, + }; + } + + private initShapes() { + const triangle_positions = [ + -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 1.0, 0.0, + ]; + + const circle_positions: number[] = [0.0, 0.0, 0.0]; + const N = 32; + const R = 1.0; + for (let i = 0; i <= N; ++i) { + const angle = ((2.0 * Math.PI) / N) * i; + circle_positions.push(R * Math.cos(angle)); + circle_positions.push(R * Math.sin(angle)); + circle_positions.push(0.0); + } + + const square_positions = [ + -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, + ]; + + const square_outline = [ + -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0, + ]; + + this.shapes.set("triangle", { + drawMode: GL.TRIANGLES, + positions: new Float32Array(triangle_positions), + outline: new Float32Array(triangle_positions), + }); + + this.shapes.set("circle", { + drawMode: GL.TRIANGLE_FAN, + positions: new Float32Array(circle_positions), + outline: new Float32Array(circle_positions.slice(3)), + }); + + this.shapes.set("square", { + drawMode: GL.TRIANGLE_STRIP, + positions: new Float32Array(square_positions), + outline: new Float32Array(square_outline), + }); + } + + protected _createModels(): Model[] { + const gl = this.context.gl; + + const shape = this.shapes.get(this.props.shape); + if (!shape) { + return this._createEmptyModels(); + } + + const shapeModel = new Model(gl, { + id: `${this.props.id}-mesh`, + vs: vsShader, + fs: fsShader, + geometry: new Geometry({ + drawMode: shape.drawMode, + attributes: { + positions: { size: 3, value: shape.positions }, + }, + }), + uniforms: { + useOutlineColor: false, + }, + modules: [project, picking, utilities], + isInstanced: true, + instanceCount: this.getNumInstances(), + }); + + const outlineModel = new Model(gl, { + id: `${this.props.id}-outline`, + vs: vsShader, + fs: fsShader, + geometry: new Geometry({ + drawMode: GL.LINE_LOOP, + attributes: { + positions: { size: 3, value: shape.outline }, + }, + }), + uniforms: { + useOutlineColor: true, + }, + modules: [project, picking, utilities], + isInstanced: true, + instanceCount: this.getNumInstances(), + }); + + return [shapeModel, outlineModel]; + } + + protected _createEmptyModels(): Model[] { + return [ + new Model(this.context.gl, { + id: `${this.props.id}-empty-mesh`, + vs: vsShader, + fs: fsShader, + isInstanced: true, + instanceCount: 0, + }), + new Model(this.context.gl, { + id: `${this.props.id}-empty-outline`, + vs: vsShader, + fs: fsShader, + isInstanced: true, + instanceCount: 0, + }), + ]; + } +} + +WellMarkersLayer.layerName = "WellMarkersLayer"; +WellMarkersLayer.defaultProps = defaultProps;