UNPKG

@openpv/simshady

Version:

Simulating Shadows for PV Potential Analysis on 3D Data on the GPU.

1 lines 79 kB
{"version":3,"sources":["../src/index.ts","../src/colormaps.ts","../src/main.ts","../src/elevation.ts","../src/sun.ts","../src/triangleUtils.ts","../src/utils.ts","../src/rayTracingWebGL.ts","../src/geometryFilter.ts"],"sourcesContent":["export * as colormaps from './colormaps';\nexport { ShadingScene } from './main';\n","import { Color, ColorMap } from './utils';\n\n/**\n * The viridis color Map, as defined in https://observablehq.com/@flimsyhat/webgl-color-maps\n * Use colormaps in the {@link ShadingScene.addColorMap} method to define the colors of the returned\n * Three.js geometry.\n * @param t parameter in [0,1] to go through viridis color map\n * @returns\n */\nexport function viridis(t: number): Color {\n t = Math.min(Math.max(t, 0), 1);\n const c0 = [0.2777273272234177, 0.005407344544966578, 0.3340998053353061];\n const c1 = [0.1050930431085774, 1.404613529898575, 1.384590162594685];\n const c2 = [-0.3308618287255563, 0.214847559468213, 0.09509516302823659];\n const c3 = [-4.634230498983486, -5.799100973351585, -19.33244095627987];\n const c4 = [6.228269936347081, 14.17993336680509, 56.69055260068105];\n const c5 = [4.776384997670288, -13.74514537774601, -65.35303263337234];\n const c6 = [-5.435455855934631, 4.645852612178535, 26.3124352495832];\n return [\n c0[0] + t * (c1[0] + t * (c2[0] + t * (c3[0] + t * (c4[0] + t * (c5[0] + t * c6[0]))))),\n c0[1] + t * (c1[1] + t * (c2[1] + t * (c3[1] + t * (c4[1] + t * (c5[1] + t * c6[1]))))),\n c0[2] + t * (c1[2] + t * (c2[2] + t * (c3[2] + t * (c4[2] + t * (c5[2] + t * c6[2]))))),\n ];\n}\n\n/**\n * Creates a color map function that interpolates between two colors.\n * Use colormaps in the {@link ShadingScene.addColorMap} method to define the colors of the returned\n * Three.js geometry.\n * @param {Object} colors - The input colors.\n * @param {Color} colors.c0 - The starting color.\n * @param {Color} colors.c1 - The ending color.\n * @returns {ColorMap} A function that takes a value t (0 to 1) and returns an interpolated color.\n */\nexport function interpolateTwoColors(colors: { c0: Color; c1: Color }): ColorMap {\n const { c0, c1 } = colors;\n\n return (t: number): Color => {\n // Clamp t between 0 and 1\n t = Math.min(Math.max(t, 0), 1);\n\n // Interpolate between c0 and c1 for R, G, and B channels\n const r = c0[0] * (1 - t) + c1[0] * t;\n const g = c0[1] * (1 - t) + c1[1] * t;\n const b = c0[2] * (1 - t) + c1[2] * t;\n\n return [r, g, b];\n };\n}\n\n/**\n * Creates a color map function that interpolates between three colors using quadratic interpolation.\n * Use colormaps in the {@link ShadingScene.addColorMap} method to define the colors of the returned\n * Three.js geometry.\n * @param {Object} colors - The input colors.\n * @param {Color} colors.c0 - The first color.\n * @param {Color} colors.c1 - The second color.\n * @param {Color} colors.c2 - The third color.\n * @returns {ColorMap} A function that takes a value t (0 to 1) and returns an interpolated color.\n */\nexport function interpolateThreeColors(colors: { c0: Color; c1: Color; c2: Color }): ColorMap {\n const { c0, c1, c2 } = colors;\n\n return (t: number): Color => {\n // Clamp t between 0 and 1\n t = Math.max(0, Math.min(1, t));\n\n function quadraticInterpolation(t: number, v0: number, v1: number, v2: number): number {\n return (1 - t) * (1 - t) * v0 + 2 * (1 - t) * t * v1 + t * t * v2;\n }\n\n const r = quadraticInterpolation(t, c0[0], c1[0], c2[0]);\n const g = quadraticInterpolation(t, c0[1], c1[1], c2[1]);\n const b = quadraticInterpolation(t, c0[2], c1[2], c2[2]);\n\n return [r, g, b];\n };\n}\n","import * as THREE from 'three';\nimport { BufferAttribute, BufferGeometry, TypedArray } from 'three';\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport { viridis } from './colormaps.js';\nimport * as elevation from './elevation.js';\nimport * as sun from './sun.js';\nimport * as triangleUtils from './triangleUtils.js';\nimport { CalculateParams, CartesianPoint, ColorMap, SolarIrradianceData, logNaNCount } from './utils.js';\n\n// @ts-ignore\nimport { rayTracingWebGL } from './rayTracingWebGL.js';\nimport { filterShadingBufferGeometry, filterShadingGeometry, getMinSunAngleFromIrradiance } from './geometryFilter';\n\n/**\n * This class holds all information about the scene that is simulated.\n * A ShadingScene is typically equipped with the following attributes:\n * * Simulation geometry, where the PV potential is calculated.\n * * Shading geometry, where no PV potential is calculated but which are\n * responsible for shading.\n * * Solar Irradiance Data that contains information about incoming irradiance\n * in the format of sky domes.\n * The Usage of this class and its methods is explained in the \"Getting Started\" Section\n * of this site.\n */\nexport class ShadingScene {\n /**\n * A Three.js geometry holding the main object of the scene,\n * see {@link ShadingScene.addShadingGeometry}\n */\n public simulationGeometry: BufferGeometry | undefined;\n /**\n * A Three.js geometry holding the objects that cause shading,\n * see {@link ShadingScene.addShadingGeometry}\n */\n public shadingGeometry: BufferGeometry | undefined;\n /**\n * The minimum radiance angle which gets used during raytracing.\n * It is being used for filtering out shading geometry which\n * physically cannot shade the simulation geometry. See {@link filterShadingGeometry}\n */\n public minSunAngle: number | undefined;\n /**\n * A Raster (2D Matrix) holding rasterized data of the terrain,\n * see {@link ShadingScene.addElevationRaster}\n */\n public elevationRaster: Array<CartesianPoint>;\n /**\n * The midpoint of the elevationRaster, where the main object of\n * the scene is located.\n * See {@link ShadingScene.addElevationRaster}\n */\n private elevationRasterMidpoint: CartesianPoint;\n /**\n * A timeseries of Skydomes holding averaged direct and diffuse\n * irradiance data.\n * See {@link ShadingScene.addSolarIrradiance}\n */\n public solarIrradiance: SolarIrradianceData[] | null;\n private colorMap: (t: number) => [number, number, number];\n\n constructor() {\n this.elevationRaster = [];\n this.elevationRasterMidpoint = { x: 0, y: 0, z: 0 };\n this.solarIrradiance = null;\n this.colorMap = viridis;\n }\n\n /**\n * Adds a geometry as a target for the shading simulation.\n * For these geometries, the PV potential will be simulated.\n * This geometry will also be used as a shading geometry, hence\n * it is not needed to additionally add it by using `addShadingGeometry`.\n *\n * @param geometry [BufferGeometry](https://threejs.org/docs/#api/en/core/BufferGeometry) of a Three.js geometry, where three\n * consecutive numbers of the array represent one 3D point and nine consecutive\n * numbers represent one triangle.\n */\n addSimulationGeometry(geometry: BufferGeometry) {\n geometry = geometry.toNonIndexed();\n if (!this.simulationGeometry) {\n this.simulationGeometry = geometry;\n } else {\n this.simulationGeometry = BufferGeometryUtils.mergeGeometries([this.simulationGeometry, geometry]);\n }\n if (!this.shadingGeometry) {\n this.shadingGeometry = geometry;\n } else {\n this.shadingGeometry = BufferGeometryUtils.mergeGeometries([this.shadingGeometry, geometry]);\n }\n }\n\n /**\n * Adds a geometry as an outer geometry for the shading simulation.\n * These geometries are responsible for shading.\n *\n * @param geometry [BufferGeometry](https://threejs.org/docs/#api/en/core/BufferGeometry) of a Three.js geometry, where three\n * consecutive numbers of the array represent one 3D point and nine consecutive\n * numbers represent one triangle.\n * @param minSunAngle The minimum radiance angle which gets used during raytracing. It is being used for filtering out\n * shading geometry which physically cannot shade the simulation geometry. If none is provided the min. angle of the\n * provided irradiance data will be used.\n */\n addShadingGeometry(geometry: BufferGeometry, minSunAngle?: number) {\n if (minSunAngle !== undefined) {\n this.minSunAngle = minSunAngle;\n }\n geometry = geometry.toNonIndexed();\n if (!this.shadingGeometry) {\n this.shadingGeometry = geometry;\n } else {\n this.shadingGeometry = BufferGeometryUtils.mergeGeometries([this.shadingGeometry, geometry]);\n }\n }\n /**\n * Add a elevation model to the simulation scene.\n * @param raster List of Points with x,y,z coordinates, representing a digital elevation model (DEM). It is\n * important that all values of x,y and z are given with same units. If x and y are given in lat / lon and\n * z is given in meters, this will result in wrong simulation Results.\n * @param midpoint The point of the observer, ie the center of the building\n * angle will be [0, ..., 2Pi] where the list has a lenght of azimuthDivisions\n */\n addElevationRaster(raster: CartesianPoint[], midpoint: CartesianPoint) {\n this.elevationRaster = raster;\n this.elevationRasterMidpoint = midpoint;\n }\n /**\n * Add data of solar irradiance to the scene. If it comes as a List of SolarIrradianceData,\n * this is interpreted as a time series of skydomes.\n *\n * **Important Note:** The first skydome of the list is used for the coloring of the final mesh!\n * Check out the type definition of {@link utils.SolarIrradianceData} for more information.\n * @param irradiance\n */\n addSolarIrradiance(irradiance: SolarIrradianceData[] | SolarIrradianceData) {\n // solarIrradiance is a time series of skydomes. If only one skydome is given\n // this one will be placed in a list\n if (!Array.isArray(irradiance)) {\n irradiance = [irradiance];\n }\n this.solarIrradiance = irradiance;\n }\n /**\n * Fetches a SolarIrradiance Object from a url and adds it to the\n * ShadingScene.\n * @param url\n */\n async addSolarIrradianceFromURL(url: string): Promise<void> {\n const response = await fetch(url);\n const data = await response.json();\n this.addSolarIrradiance(data);\n }\n\n /**\n * Change the Color Map that is used for the colors of the simulated Three.js mesh. This is\n * optional, the default colorMap is viridis (blue to green to yellow). Other options are\n * {@link colormaps.interpolateTwoColors} or {@link colormaps.interpolateThreeColors}\n * @param colorMap\n */\n addColorMap(colorMap: ColorMap) {\n this.colorMap = colorMap;\n }\n\n /** @ignore\n * Gets a BufferGeometry representing a mesh. Refines the triangles until all triangles\n * have sites smaller maxLength.\n */\n\n refineMesh(mesh: BufferGeometry, maxLength: number): BufferGeometry {\n const positions = mesh.attributes.position.array.slice();\n\n const newTriangles: number[] = [];\n const newNormals: number[] = [];\n // Iterate over triangles\n for (let i = 0; i < positions.length; i += 9) {\n const normal = triangleUtils.normal(positions, i);\n if (normal[2] < -0.9) {\n // Triangle is facing down, we can skip this\n continue;\n }\n const triangles = triangleUtils.subdivide(positions, i, maxLength);\n for (let j = 0; j < triangles.length; j++) {\n newTriangles.push(triangles[j]);\n // copy normal for each subdivided triangle\n newNormals.push(normal[j % 3]);\n }\n }\n\n const geometry = new BufferGeometry();\n const normalsArray = new Float32Array(newNormals);\n const positionArray = new Float32Array(newTriangles);\n geometry.setAttribute('position', new BufferAttribute(positionArray, 3));\n geometry.setAttribute('normal', new BufferAttribute(normalsArray, 3));\n geometry.attributes.position.needsUpdate = true;\n geometry.attributes.normal.needsUpdate = true;\n\n return geometry;\n }\n\n /**\n * This function is called as a last step, after the scene is fully build.\n * It runs the shading simulation and returns a THREE.js colored mesh.\n * The colors are chosen from the defined colorMap.\n * @param params The input object containing information about the simulation.\n\n * @returns A three.js colored mesh of the simulationGeometry. Each triangle gets an \n * attribute called intensity, that holds the annual electricity in kwh/m2 that a PV\n * system can generate. If {@link ShadingScene.solarIrradiance} is a timeseries of sky\n * domes, the resulting intensities attribute is a flattened Float32Array of length T*N.\n */\n async calculate(params: CalculateParams = {}) {\n const {\n solarToElectricityConversionEfficiency = 0.15,\n maxYieldPerSquareMeter = 1400 * 0.15,\n progressCallback = (progress, total, elapsed, remaining) => {\n const format = (s: number) => {\n const min = Math.floor(s / 60);\n const sec = Math.floor(s % 60);\n return min > 0 ? `${min}m ${sec}s` : `${sec}s`;\n };\n console.log(`Progress: ${progress}/${total} | Elapsed: ${format(elapsed)} | Est. remaining: ${format(remaining)}`);\n },\n } = params;\n\n // Validate class parameters\n if (!this.validateClassParams()) {\n throw new Error(\n 'Invalid Class Parameters: You need to supply at least Shading Geometry, a Simulation Geometry, and Irradiance Data.',\n );\n }\n\n //Filter out irrelevant shading geometry\n const minSunAngle = this.minSunAngle ?? getMinSunAngleFromIrradiance(this.solarIrradiance);\n this.shadingGeometry = filterShadingBufferGeometry(this.simulationGeometry, this.shadingGeometry, minSunAngle);\n\n // Merge geometries\n this.simulationGeometry = this.refineMesh(this.simulationGeometry, 1.0);\n\n // Extract and validate geometry attributes\n // Flattened Mx3 array for M points\n const meshArray = <Float32Array>this.shadingGeometry.attributes.position.array;\n // Flattened Nx3 array for N points\n const points = this.simulationGeometry.attributes.position.array;\n // Flattened (N/3)x3 array for N/3 triangles, each triangle with a normal\n // Originally, every N point has one normal\n // Keeping only the first 3 elements out of every 9 so we have one normal\n // per triangle, not one normal per triangle edge point\n const normalsArray = this.simulationGeometry.attributes.normal.array.filter((_, index) => index % 9 < 3);\n\n const midpointsArray = this.computeMidpoints(points);\n\n // Check for NaN values in geometry data\n logNaNCount('midpoints', midpointsArray);\n logNaNCount('mesh', meshArray);\n\n // Wrap progress callback with timing\n const startTime = Date.now();\n const wrappedCallback = (progress: number, total: number) => {\n if (progress === 0) {\n return;\n }\n const elapsed = (Date.now() - startTime) / 1000;\n const average = elapsed / progress;\n const remaining = Math.max(0, (total - progress) * average);\n progressCallback(progress, total, elapsed, remaining);\n };\n\n // Perform ray tracing to calculate intensities\n const shadedScene = await this.rayTrace(\n midpointsArray,\n normalsArray,\n meshArray,\n this.solarIrradiance!, // Non-null assertion\n wrappedCallback,\n );\n\n const pvYield = sun.calculatePVYield(\n shadedScene,\n solarToElectricityConversionEfficiency,\n this.solarIrradiance[0].metadata.valid_timesteps_for_aggregation,\n );\n\n return this.createMesh(this.simulationGeometry, pvYield, maxYieldPerSquareMeter);\n }\n\n // Type Guard function to validate class parameters\n private validateClassParams(): this is {\n shadingGeometry: NonNullable<BufferGeometry>;\n simulationGeometry: NonNullable<BufferGeometry>;\n solarIrradiance: NonNullable<SolarIrradianceData>;\n } {\n return (\n this.shadingGeometry !== null &&\n this.shadingGeometry !== undefined &&\n this.simulationGeometry !== null &&\n this.simulationGeometry !== undefined &&\n this.solarIrradiance != null\n );\n }\n\n // Helper to compute midpoints of triangles and track NaN values\n private computeMidpoints(points: TypedArray): Float32Array {\n let midpoints: number[] = [];\n for (let i = 0; i < points.length; i += 9) {\n const midpoint = triangleUtils.midpoint(points, i);\n midpoints.push(...midpoint);\n }\n return new Float32Array(midpoints);\n }\n\n /**\n * @ignore\n * This function does two things:\n * - it assigns a color to the given simulationGeometry. The color is assigned\n * using the FIRST value of the intensities time series and the maxYieldPerSquareMeter\n * as upper boundary.\n * - it flattens the time series of intensities and sets them as attribute to the simulationGeometry\n *\n * @param simulationGeometry Nx9 Array with the edge points of N triangles\n * @param intensities T x N intensities, one for every triangle and every time step\n * @param maxYieldPerSquareMeter number defining the upper boundary of the color map\n * @returns Mesh with color and new attribute \"intensities\" that has length T*N\n */\n private createMesh(\n simulationGeometry: BufferGeometry,\n intensities: Float32Array[],\n maxYieldPerSquareMeter: number,\n ): THREE.Mesh {\n const Npoints = simulationGeometry.attributes.position.array.length / 9;\n var newColors = new Float32Array(Npoints * 9);\n\n for (var i = 0; i < Npoints; i++) {\n const col = this.colorMap(Math.min(maxYieldPerSquareMeter, intensities[0][i]) / maxYieldPerSquareMeter);\n for (let j = 0; j < 9; j += 3) {\n newColors[9 * i + j] = col[0];\n newColors[9 * i + j + 1] = col[1];\n newColors[9 * i + j + 2] = col[2];\n }\n }\n\n simulationGeometry.setAttribute('color', new THREE.Float32BufferAttribute(newColors, 3));\n var material = new THREE.MeshStandardMaterial({\n vertexColors: true,\n side: THREE.DoubleSide,\n });\n // In THREE, only Flat arrays can be set as an attribute\n const flatIntensities = new Float32Array(intensities.map((arr) => Array.from(arr)).flat());\n\n // Set the T*N Float32Array of intensities as attributes. On the website, this intensities\n // attribute needs to be divided again in T parts for the T time steps.\n simulationGeometry.setAttribute('intensities', new THREE.Float32BufferAttribute(flatIntensities, 1));\n let mesh = new THREE.Mesh(simulationGeometry, material);\n\n return mesh;\n }\n\n /** @ignore\n * This function returns a time series of intensities of shape T x N, with N the number of midpoints.\n * It includes the shading of geometries, the dot product of normal vector and sky segment vector,\n * and the radiation values from diffuse and direct irradiance.\n *\n * @param midpoints midpoints of triangles for which to calculate intensities\n * @param normals normals for each midpoint\n * @param meshArray array of vertices for the shading mesh\n * @param irradiance Time Series of sky domes\n * @return\n */\n private async rayTrace(\n midpoints: Float32Array,\n normals: TypedArray,\n meshArray: Float32Array,\n irradiance: SolarIrradianceData[],\n progressCallback: (progress: number, total: number) => void,\n ): Promise<Float32Array[]> {\n /**\n * Converts a list of solarIrradiance objects to a flat Float32Array containing only\n * the normalized cartesian coordinates (x, y, z) of the skysegments\n * and a list of Float32Arrays containing the absolute values of radiances at each\n * sky segment.\n * @param solarIrradiance\n * @returns skysegmentDirections as Sx3 flattened Float32Array with S being number of skysegments\n * skysegmentRadiation List of Float32Array, List has lenght T as the number of time steps,\n * each Float32Array has lenght S with on radiation value for each segment\n */\n function convertSolarIrradianceToFloat32Array(solarIrradiance: SolarIrradianceData[]): {\n skysegmentDirections: Float32Array;\n skysegmentRadiation: Float32Array[];\n } {\n const directions: number[] = [];\n const radiation: Float32Array[] = [];\n\n for (const entry of solarIrradiance) {\n const radiances: number[] = [];\n for (const point of entry.data) {\n const altRad = (point.altitude_deg * Math.PI) / 180;\n const azRad = (point.azimuth_deg * Math.PI) / 180;\n\n const x = Math.cos(altRad) * Math.sin(azRad);\n const y = Math.cos(altRad) * Math.cos(azRad);\n const z = Math.sin(altRad);\n\n directions.push(x, y, z);\n radiances.push(point.average_radiance_W_m2_sr);\n }\n radiation.push(new Float32Array(radiances));\n }\n\n return {\n skysegmentDirections: new Float32Array(directions),\n skysegmentRadiation: radiation,\n };\n }\n\n // Convert the existing array to a flat Float32Array\n const { skysegmentDirections, skysegmentRadiation } = convertSolarIrradianceToFloat32Array(irradiance);\n\n const shadedMaskScenes = await rayTracingWebGL(midpoints, normals, meshArray, skysegmentDirections, progressCallback);\n if (shadedMaskScenes === null) {\n throw new Error('Error occured when running the Raytracing in WebGL.');\n }\n //TODO Insert shading from ELevationRaster here\n if (this.elevationRaster.length > 0) {\n const elevationShadingMask = elevation.getElevationShadingMask(\n this.elevationRaster,\n this.elevationRasterMidpoint,\n // extract the altitude azimuth pairs from the first skysegment\n irradiance[0].data.map(({ altitude_deg, azimuth_deg }) => [altitude_deg, azimuth_deg]),\n );\n }\n\n //At this point we have one shaded mask array (length N) of normalized vectors\n //for the sky segment\n //And a time series of skySegmentRadiation (which are the absolute values of the sky segment\n // vectors)\n\n // Initializize Intensities of shape T x N, with one intensity per time step per midpoint\n let intensities = skysegmentRadiation.map(() => new Float32Array(midpoints.length / 3));\n\n //iterate over each sky segment\n for (let i = 0; i < shadedMaskScenes.length; i++) {\n // iterate over each midpoint\n for (let j = 0; j < midpoints.length; j++) {\n for (let t = 0; t < intensities.length; t++) {\n intensities[t][j] += shadedMaskScenes[i][j] * skysegmentRadiation[t][i];\n }\n }\n }\n return intensities;\n }\n}\n","import { CartesianPoint, SphericalPoint } from './utils';\n\nexport function fillMissingAltitudes(maxAngles: SphericalPoint[]): void {\n // First copy the maxAngles to a newAngles list, so that changes\n // in the list do not affect the algorithm\n let newAngles = maxAngles.map((angle) => ({ ...angle }));\n for (let i = 0; i < newAngles.length; i++) {\n if (newAngles[i].altitude != -Infinity) {\n continue;\n }\n let distance = 1;\n while (true) {\n let prevIndex = (i - distance + newAngles.length) % newAngles.length;\n let nextIndex = (i + distance) % newAngles.length;\n\n if (maxAngles[nextIndex].altitude !== -Infinity) {\n newAngles[i].altitude = maxAngles[nextIndex].altitude;\n break;\n } else if (maxAngles[prevIndex].altitude !== -Infinity) {\n newAngles[i].altitude = maxAngles[prevIndex].altitude;\n break;\n } else distance++;\n }\n }\n // Overwrite the maxAngles to make changes in this vector global\n for (let i = 0; i < maxAngles.length; i++) {\n maxAngles[i] = newAngles[i];\n }\n}\n\n/**\n * Returns the vector from start to end in the Horizontal coordinate system\n * @param start\n * @param end\n * @returns\n */\nexport function calculateSphericalCoordinates(start: CartesianPoint, end: CartesianPoint): SphericalPoint {\n const dx = end.x - start.x;\n const dy = end.y - start.y;\n const dz = end.z - start.z;\n if (dx == 0 && dy == 0) {\n return { radius: 1, azimuth: 0, altitude: 0 };\n }\n\n const r = Math.sqrt(dx * dx + dy * dy + dz * dz);\n const altitude = Math.asin(dz / r);\n\n let azimuth = (2 * Math.PI - Math.atan2(dy, dx)) % (2 * Math.PI);\n\n return { radius: 1, azimuth, altitude };\n}\n\n/**\n * Calculates the maximum heights visible from an observer in a set of directions.\n * Returns a list of spherical points of length numDirections.\n * @param elevation list of points with x,y,z component\n * @param observer Point of interest for which the elevation angles are calculated.\n * @param directions List of altitude azimuth pairs. Angles in degree and conform to the\n * coordinate space definition of simshady.\n * @returns\n */\nexport function getElevationShadingMask(\n elevation: CartesianPoint[],\n observer: CartesianPoint,\n directions: [number, number][],\n): [number, number, number][] {\n const shadingMask: [number, number, number][] = [];\n\n for (const [altDeg, azDeg] of directions) {\n const azRad = (azDeg * Math.PI) / 180;\n const altRad = (altDeg * Math.PI) / 180;\n let maxAltitude = -Infinity;\n\n for (const point of elevation) {\n const { azimuth, altitude } = calculateSphericalCoordinates(observer, point);\n const azDiff = Math.abs(((azimuth - azRad + Math.PI) % (2 * Math.PI)) - Math.PI);\n if (azDiff < Math.PI / 180) {\n // approx 1 degree tolerance\n if (altitude > maxAltitude) maxAltitude = altitude;\n }\n }\n\n const isVisible = altRad > maxAltitude ? 1 : 0;\n shadingMask.push([altDeg, azDeg, isVisible]);\n }\n\n return shadingMask;\n}\n","import { SolarIrradianceData, SphericalPoint, SunVector } from './utils';\n\nexport async function fetchIrradiance(baseUrl: string, lat: number, lon: number): Promise<SolarIrradianceData> {\n const url = baseUrl + '/' + lat.toFixed(0) + '.0/' + lon.toFixed(0) + '.0.json';\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error('Network response was not ok');\n }\n const jsonData = await response.json();\n return jsonData;\n } catch (error) {\n console.error('There was a problem with the fetch operation:', error);\n throw error;\n }\n}\n\nexport function shadeIrradianceFromElevation(Irradiance: SunVector[], shadingElevationAngles: SphericalPoint[]): void {\n function findShadingElevation(azimuth: number): SphericalPoint {\n return shadingElevationAngles.reduce((prev, curr) =>\n Math.abs(curr.azimuth - azimuth) < Math.abs(prev.azimuth - azimuth) ? curr : prev,\n );\n }\n\n for (let i = Irradiance.length - 1; i >= 0; i--) {\n const point = Irradiance[i];\n const shadingElevation = findShadingElevation(point.vector.spherical.azimuth);\n if (shadingElevation && point.vector.spherical.altitude < shadingElevation.altitude) {\n Irradiance[i].isShadedByElevation = true;\n }\n }\n}\n\n/**\n * Converts intensities, which are solar energy per time per area, into\n * electric energy per time per area. This means multiplying every element of\n * intensities with the solarToElectricityConversionEfficiency.\n * @param intensities\n * @param solarToElectricityConversionEfficiency\n * @returns\n */\nexport function calculatePVYield(\n intensities: Float32Array[],\n solarToElectricityConversionEfficiency: number,\n totalHours: number,\n): Float32Array[] {\n // solarIrradiance.metadata.valid_timesteps_for_aggregation is hours over which you have to aggregate the mean intensity\n // 0.065 is solid angle (Raumwinkel) per skypixel in the HEALpix Level 4\n // 1/1000 is Watt to kiloWatt\n const factor = ((totalHours * 0.065) / 1000) * solarToElectricityConversionEfficiency;\n return intensities.map((arr) => new Float32Array(arr.map((x) => x * factor)));\n}\n","export type ArrayType =\n | Int8Array\n | Uint8Array\n | Uint8ClampedArray\n | Int16Array\n | Uint16Array\n | Int32Array\n | Uint32Array\n | Float32Array\n | Float64Array\n | number[];\n\nexport type Triangle = [number, number, number, number, number, number, number, number, number];\n\nexport function normalAndArea(positions: ArrayType, startIndex: number): [[number, number, number], number] {\n const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = positions.slice(startIndex, startIndex + 9);\n\n const d01x = x1 - x0;\n const d01y = y1 - y0;\n const d01z = z1 - z0;\n const d02x = x2 - x0;\n const d02y = y2 - y0;\n const d02z = z2 - z0;\n\n const crsx = d01y * d02z - d01z * d02y;\n const crsy = d01z * d02x - d01x * d02z;\n const crsz = d01x * d02y - d01y * d02x;\n\n const crs_norm = Math.sqrt(crsx * crsx + crsy * crsy + crsz * crsz);\n const area = crs_norm / 2;\n const normal: [number, number, number] = [crsx / crs_norm, crsy / crs_norm, crsz / crs_norm];\n return [normal, area];\n}\n\nexport function normal(positions: ArrayType, startIndex: number): [number, number, number] {\n return normalAndArea(positions, startIndex)[0];\n}\n\nexport function area(positions: ArrayType, startIndex: number): number {\n return normalAndArea(positions, startIndex)[1];\n}\n\n/**\n *\n * Takes an array of triangles and subdivides one triangle into smaller triangles. The algorithm finds the longest edge of the\n * triangle and splits it at its center, creating two new triangles. This process continues until none of the edges of\n * the resulting triangles are longer then the threshold.\n *\n * @param positions Array containing the triangles vertex positions.\n * @param startIndex The starting index in the positions array where the triangle begins.\n * @param threshold The maximum allowed edge length.\n */\nexport function subdivide(positions: ArrayType, startIndex: number, threshold: number): number[] {\n const result: number[] = [];\n const stack: number[][] = [];\n\n const initialTriangle = Array.from(positions.slice(startIndex, startIndex + 9));\n stack.push(initialTriangle);\n\n while (stack.length > 0) {\n const triangle = stack.pop()!;\n const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = triangle;\n\n const d01x = x1 - x0;\n const d01y = y1 - y0;\n const d01z = z1 - z0;\n const d02x = x2 - x0;\n const d02y = y2 - y0;\n const d02z = z2 - z0;\n const d12x = x2 - x1;\n const d12y = y2 - y1;\n const d12z = z2 - z1;\n\n const l01 = d01x * d01x + d01y * d01y + d01z * d01z;\n const l02 = d02x * d02x + d02y * d02y + d02z * d02z;\n const l12 = d12x * d12x + d12y * d12y + d12z * d12z;\n\n const longest = Math.max(l01, l02, l12);\n if (longest <= threshold * threshold) {\n result.push(...triangle);\n continue;\n }\n\n if (l01 === longest) {\n const xm = (x0 + x1) / 2;\n const ym = (y0 + y1) / 2;\n const zm = (z0 + z1) / 2;\n\n const tri1 = [x0, y0, z0, xm, ym, zm, x2, y2, z2];\n const tri2 = [x1, y1, z1, x2, y2, z2, xm, ym, zm];\n\n stack.push(tri1, tri2);\n } else if (l02 === longest) {\n const xm = (x0 + x2) / 2;\n const ym = (y0 + y2) / 2;\n const zm = (z0 + z2) / 2;\n\n const tri1 = [x0, y0, z0, x1, y1, z1, xm, ym, zm];\n const tri2 = [x1, y1, z1, x2, y2, z2, xm, ym, zm];\n\n stack.push(tri1, tri2);\n } else if (l12 === longest) {\n const xm = (x1 + x2) / 2;\n const ym = (y1 + y2) / 2;\n const zm = (z1 + z2) / 2;\n\n const tri1 = [x0, y0, z0, x1, y1, z1, xm, ym, zm];\n const tri2 = [x2, y2, z2, x0, y0, z0, xm, ym, zm];\n\n stack.push(tri1, tri2);\n } else {\n throw new Error(\"No edge is longest, this shouldn't happen\");\n }\n }\n return result;\n}\n\nexport function midpoint(positions: ArrayType, startIndex: number): [number, number, number] {\n const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = positions.slice(startIndex, startIndex + 9);\n return [(x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3, (z0 + z1 + z2) / 3];\n}\n","/**\n * Solar irradiance data. `metadata` json holds the coordinates\n * where the irradiance data can be used. valid_timesteps_for_aggregation\n * is the number of hours of daylight in the considered timeframe. If\n * the skydome represents a whole year, this is about 8760.\n * \n * `data` holds a list of\n * sky segments, where altitude_deg and azimuth_deg define the position\n * and average_radiance_W_m2_sr defines the averaged incoming irradinace in W per m2 per sr. \n * Read more about it in the \"How does simshady work\" section of the docs page.\n *\n * Definition of the coordiante system in `simshady`:\n * Angles are expected in degree.\n * Azimuth = 0 is North, Azimuth = 90° is East.\n * Altitude = 0 is the horizon, Altitude = 90° is upwards / Zenith.\n * \n * Example Data:\n * ```json\n * {\n \"data\": [\n {\n \"altitude_deg\": 78.28,\n \"azimuth_deg\": 45.0,\n \"average_radiance_W_m2_sr\": 17.034986301369866\n },\n {\n \"altitude_deg\": 78.28,\n \"azimuth_deg\": 315.0,\n \"average_radiance_W_m2_sr\": 17.034986301369866\n },\n ...\n ],\n \"metadata\": {\n \"latitude\": 49.8,\n \"longitude\": 8.6,\n \"valid_timesteps_for_aggregation\": 8760,\n }\n}\n ```\n */\nexport type SolarIrradianceData = {\n metadata: { latitude: number; longitude: number; valid_timesteps_for_aggregation: number };\n data: Array<{ altitude_deg: number; azimuth_deg: number; average_radiance_W_m2_sr: number }>;\n};\n\n/**\n * Spherical Coordinate of a point.\n *\n * Azimuth = 0 is North, Azimuth = PI/2 is East.\n *\n * Altitude = 0 is the horizon, Altitude = PI/2 is upwards / Zenith.\n */\nexport type SphericalPoint = { radius: number; altitude: number; azimuth: number };\n\n/**\n * Cartesian Coordinate of a point.\n *\n * Positive X-axis is east.\n * Positive Y-axis is north.\n * Positive z-axis is upwards.\n */\nexport type CartesianPoint = { x: number; y: number; z: number };\n\n/**\n @ignore\n */\nexport type Point = { cartesian: CartesianPoint; spherical: SphericalPoint };\n\n/**\n @ignore\n */\nexport type SunVector = { vector: Point; isShadedByElevation: boolean };\n\n/**\n * RGB values of a color, where all values are in intervall [0,1].\n */\nexport type Color = [number, number, number];\n\n/**\n * A color Map maps a value t in [0,1] to a color.\n */\nexport type ColorMap = (t: number) => Color;\n\n/**\n * Interface for the parameter object for {@link index.ShadingScene.calculate}\n */\nexport interface CalculateParams {\n /**\n * Efficiency of the conversion from solar energy to electricity. This includes the\n * pv cell efficiency (about 20%) as well as the coverage density of PV panels per area\n * (about 70%).\n * Value in [0,1].\n * @defaultValue 0.15\n */\n solarToElectricityConversionEfficiency?: number;\n /**\n * Upper boundary of annual yield in kWh/m2/year. This value is used to normalize\n * the color of the returned three.js mesh.\n * In Germany this is something like 1400 kWh/m2/year multiplied with the given\n * solarToElectricityConversionEfficiency.\n * @defaultValue 1400*0.15\n */\n maxYieldPerSquareMeter?: number;\n /**\n * Callback function to indicate the progress of the simulation.\n * @param progress Number indicating the current progress.\n * @param total Number indicating the final number that progress needs to reach.\n * @param elapsed Elapsed time in seconds since simulation start.\n * @param remaining Estimated remaining time in seconds.\n * @returns\n */\n progressCallback?: (progress: number, total: number, elapsed: number, remaining: number) => void;\n}\n\n/**\n * @ignore\n * Mimics a for-loop but schedules each loop iteration using `setTimeout`, so that\n * event handles, react updates, etc. can run in-between.\n */\nexport async function timeoutForLoop(start: number, end: number, body: (i: number) => void, step: number = 1) {\n return new Promise<void>((resolve) => {\n const inner = (i: number) => {\n body(i);\n i = i + step;\n if (i >= end) {\n resolve();\n } else {\n setTimeout(() => inner(i), 0);\n }\n };\n setTimeout(() => inner(start), 0);\n });\n}\n\n/**\n * @ignore\n * Helper to log NaN counts in data arrays. If no NaN values are found\n * nothing is logged.\n */\nexport function logNaNCount(name: string, array: Float32Array): void {\n const nanCount = Array.from(array).filter(isNaN).length;\n if (nanCount > 0) {\n console.log(`${nanCount}/${array.length} ${name} coordinates are NaN`);\n }\n}\n","import { TypedArray } from 'three';\nimport { timeoutForLoop } from './utils';\n\n/**\n * this function calculates\n * 1) for each surface point\n * 2) for each sky segment\n * 3) the dot product of normalized skysegment direction vector and the normal of the surface point\n * 4) if the sky segment is visible from the point\n * @param midpointsArray flattened Nx3 array with N the number of surface points for which shading is calculated\n * @param normals flattened Nx3 array, N times a vector of 3 components (the midpoint normals)\n * @param trianglesArray flattened Mx9 array with M the number of shading triangles\n * @param skysegmentDirectionArray flattened Sx3 array with S being the number of sky segments (normalized!)\n * @param progressCallback flattened Callback indicating the progress\n * @returns shadedMaskScenes: NxS array containing the dot product of the direction vector of the skysegment and the normal of the midpoint\n */\nexport async function rayTracingWebGL(\n midpointsArray: TypedArray,\n normals: TypedArray,\n trianglesArray: TypedArray,\n skysegmentDirectionArray: Float32Array,\n progressCallback: (progress: number, total: number) => void,\n): Promise<Float32Array[] | null> {\n const N_TRIANGLES = trianglesArray.length / 9;\n const width = midpointsArray.length / 3; // Change this to the number of horizontal points in the grid\n const N_POINTS = width;\n\n const gl = document.createElement('canvas').getContext('webgl2');\n if (!gl) {\n throw new Error('Browser does not support WebGL2');\n }\n\n // Vertex shader code\n const vertexShaderSource = `#version 300 es\n #define INFINITY 1000000.0\n precision highp float;\n\n\n uniform sampler2D u_triangles;\n uniform vec3 u_sun_direction;\n uniform int textureWidth;\n uniform int u_triangleStart;\n uniform int u_triangleCount;\n\n in vec3 a_position;\n in vec3 a_normal;\n\n out vec4 outColor;\n\n vec3 cross1(vec3 a, vec3 b) {\n vec3 c = vec3(0, 0, 0);\n c.x = a[1] * b[2] - a[2] * b[1];\n c.y = a[2] * b[0] - a[0] * b[2];\n c.z = a[0] * b[1] - a[1] * b[0];\n return c;\n }\n\n float TriangleIntersect( vec3 v0, vec3 v1, vec3 v2, vec3 rayOrigin, vec3 rayDirection, int isDoubleSided )\n {\n vec3 edge1 = v1 - v0;\n vec3 edge2 = v2 - v0;\n\n vec3 pvec = cross(rayDirection, edge2);\n\n float epsilon = 0.000001; // Add epsilon to avoid division by zero\n float det = dot(edge1, pvec);\n if (abs(det) < epsilon) // Check if det is too close to zero\n return INFINITY;\n\n float inv_det = 1.0 / det;\n if ( isDoubleSided == 0 && det < 0.0 ) \n return INFINITY;\n\n vec3 tvec = rayOrigin - v0;\n float u = dot(tvec, pvec) * inv_det;\n vec3 qvec = cross(tvec, edge1);\n float v = dot(rayDirection, qvec) * inv_det;\n float t = dot(edge2, qvec) * inv_det;\n float x = dot(pvec,pvec);\n return (u < 0.0 || u > 1.0 || v < 0.0 || u + v > 1.0 || t <= 0.01) ? INFINITY : t;\n\n }\n\n\n bool Calculate_Shading_at_Point(vec3 vertex_position, vec3 sun_direction) {\n float d;\n float t = INFINITY;\n bool is_shadowed = false;\n for (int i = 0; i < ${N_TRIANGLES}; i++) {\n if (i >= u_triangleCount) { break; }\n int tri = u_triangleStart + i;\n\n int index = tri * 3;\n int x = index % textureWidth;\n int y = index / textureWidth;\n vec3 v0 = texelFetch(u_triangles, ivec2(x, y), 0).rgb;\n\n index = tri * 3 + 1;\n x = index % textureWidth;\n y = index / textureWidth;\n vec3 v1 = texelFetch(u_triangles, ivec2(x, y), 0).rgb;\n\n index = tri * 3 + 2;\n x = index % textureWidth;\n y = index / textureWidth;\n vec3 v2 = texelFetch(u_triangles, ivec2(x, y), 0).rgb;\n d = TriangleIntersect(v0, v1, v2, vertex_position, sun_direction, 1);\n if (d < t && abs(d)>0.0001) {\n return true;\n\n }\n }\n return is_shadowed;\n }\n\n void main() {\n if (Calculate_Shading_at_Point(a_position.xyz, u_sun_direction)) {\n outColor = vec4(0, 0, 0, 0); // Shadowed\n } else {\n float intensity = abs(dot(a_normal.xyz, u_sun_direction));\n outColor = vec4(intensity, intensity, intensity, intensity); // Not shadowed\n }\n\n }`;\n\n // Fragment shader code\n const fragmentShaderSource = `#version 300 es\n precision highp float;\n void main() {\n }\n `;\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);\n\n const program = createProgram(gl, vertexShader, fragmentShader, ['outColor']);\n\n const vao = gl.createVertexArray();\n gl.bindVertexArray(vao);\n\n var maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);\n\n var textureWidth = Math.min(3 * N_TRIANGLES, maxTextureSize);\n var textureHeight = Math.ceil((3 * N_TRIANGLES) / textureWidth);\n\n const colorBuffer = makeBuffer(gl, N_POINTS * 16);\n const tf = makeTransformFeedback(gl, colorBuffer);\n // gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);\n // gl.pixelStorei(gl.PACK_ALIGNMENT, 1);\n\n gl.useProgram(program);\n\n var texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n\n var alignedTrianglesArray;\n if (textureHeight == 1) {\n alignedTrianglesArray = trianglesArray;\n } else {\n alignedTrianglesArray = new Float32Array(textureWidth * textureHeight * 3);\n\n for (var i = 0; i < 3 * N_TRIANGLES; i++) {\n var x = (3 * i) % textureWidth;\n var y = Math.floor((3 * i) / textureWidth);\n var index = y * textureWidth + x;\n for (var j = 0; j < 3; j++) {\n alignedTrianglesArray[index + j] = trianglesArray[3 * i + j];\n }\n }\n }\n\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB32F, textureWidth, textureHeight, 0, gl.RGB, gl.FLOAT, alignedTrianglesArray);\n\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n gl.bindTexture(gl.TEXTURE_2D, null);\n\n var u_trianglesLocation = gl.getUniformLocation(program, 'u_triangles');\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.uniform1i(u_trianglesLocation, 0);\n\n var u_textureWidth = gl.getUniformLocation(program, 'textureWidth');\n gl.uniform1i(u_textureWidth, textureWidth);\n\n const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');\n const normalAttributeLocation = gl.getAttribLocation(program, 'a_normal');\n\n const positionBuffer = makeBufferAndSetAttribute(gl, midpointsArray, positionAttributeLocation);\n const normalBuffer = makeBufferAndSetAttribute(gl, normals, normalAttributeLocation);\n\n //var colorCodedArray = null;\n var shadedMaskScenes: Float32Array[] = [];\n // each element of this shadedIrradianceScenes represents the shading\n // caused by one ray of the irradiance list\n\n // Chunking approach; reduce number of triangles per pass\n const MAX_TRIANGLES_PER_PASS = 4096; // 4k triangles per pass. Currently hardcoded\n const passCount = Math.max(1, Math.ceil(N_TRIANGLES / MAX_TRIANGLES_PER_PASS));\n const u_triangleStartLocation = gl.getUniformLocation(program, 'u_triangleStart');\n const u_triangleCountLocation = gl.getUniformLocation(program, 'u_triangleCount');\n\n await timeoutForLoop(\n 0,\n skysegmentDirectionArray.length,\n (i) => {\n // For the progress callback, we need to report the current vector index (i/3)\n // out of the total number of vectors (length/3)\n progressCallback(Math.floor(i / 3), Math.floor(skysegmentDirectionArray.length / 3));\n\n let x = skysegmentDirectionArray[i];\n let y = skysegmentDirectionArray[i + 1];\n let z = skysegmentDirectionArray[i + 2];\n\n let magnitude = Math.sqrt(x * x + y * y + z * z);\n\n // Handle zero or near-zero magnitude\n if (magnitude < 1e-10) {\n // Default direction if vector is effectively zero\n x = 0;\n y = 0;\n z = 1;\n } else {\n x = x / magnitude;\n y = y / magnitude;\n z = z / magnitude;\n }\n\n let sunDirectionUniformLocation = gl.getUniformLocation(program, 'u_sun_direction');\n gl.uniform3fv(sunDirectionUniformLocation, [x, y, z]);\n\n // Nested loop for better GPU handling\n let shadedMaskScene: Float32Array | null = null;\n for (let pass = 0; pass < passCount; pass++) {\n const start = pass * MAX_TRIANGLES_PER_PASS;\n const count = Math.min(MAX_TRIANGLES_PER_PASS, N_TRIANGLES - start);\n gl.uniform1i(u_triangleStartLocation, start);\n gl.uniform1i(u_triangleCountLocation, count);\n drawArraysWithTransformFeedback(gl, tf, gl.POINTS, N_POINTS);\n let colorCodedArray = getResults(gl, colorBuffer, N_POINTS);\n // Apply maximum shadowing (min. intensity) to triangle\n let intensities = colorCodedArray.filter((_, index) => (index + 1) % 4 === 0);\n if (shadedMaskScene === null) {\n shadedMaskScene = intensities;\n } else {\n for (let k = 0; k < shadedMaskScene.length; k++) {\n // Apply minimal found intensity\n shadedMaskScene[k] = Math.min(shadedMaskScene[k], intensities[k]);\n }\n }\n }\n // Store the result at the correct index in shadedMaskScenes (i/3 for the vector index)\n shadedMaskScenes[Math.floor(i / 3)] = shadedMaskScene as Float32Array;\n },\n 3,\n ); // Add step parameter of 3 for the loop\n\n gl.deleteTexture(texture);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n gl.deleteProgram(program);\n gl.deleteBuffer(positionBuffer);\n gl.deleteBuffer(normalBuffer);\n gl.deleteTransformFeedback(tf);\n gl.deleteBuffer(colorBuffer);\n return shadedMaskScenes;\n}\n\nfunction getResults(gl: WebGL2RenderingContext, buffer: WebGLBuffer | null, N_POINTS: number) {\n let results = new Float32Array(N_POINTS * 4);\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.getBufferSubData(\n gl.ARRAY_BUFFER,\n 0, // byte offset into GPU buffer,\n results,\n );\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null); // productBuffer was still bound to ARRAY_BUFFER so unbind it\n return results;\n}\n\nfunction createShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type);\n if (shader === null) {\n return null;\n }\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\n if (success) {\n return shader;\n }\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n}\n\nfunction createProgram(\n gl: WebGL2RenderingContext,\n vertexShader: WebGLShader | null,\n fragmentShader: WebGLShader | null,\n variables_of_interest: Iterable<string>,\n): WebGLProgram {\n const program = gl.createProgram();\n\n if (program === null || vertexShader === null || fragmentShader === null) {\n throw new Error('abortSimulation');\n } else {\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.transformFeedbackVaryings(program, variables_of_interest, gl.SEPARATE_ATTRIBS);\n gl.linkProgram(program);\n const success = gl.getProgramParameter(program, gl.LINK_STATUS);\n if (success) {\n return program;\n }\n console.error(gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n }\n throw new Error('Program compilation error.');\n}\n\nfunction makeBuffer(gl: WebGL2RenderingContext, sizeOrData: BufferSource | number) {\n const buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n //@ts-ignore\n gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, gl.DYNAMIC_DRAW);\n return buf;\n}\n\nfunction makeTransformFeedback(gl: WebGL2RenderingContext, buffer: WebGLBuffer | null) {\n const tf = gl.createTransformFeedback();\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer);\n return tf;\n}\n\nfunction makeBufferAndSetAttribute(gl: WebGL2RenderingContext, data: ArrayBuffer, loc: number): WebGLBuffer | null {\n const buf = makeBuffer(gl, data);\n // setup our attributes to tell WebGL how to pull\n // the data from the buffer above to the attribute\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(\n loc,\n 3, // size (num components)\n gl.FLOAT, // type of data in buffer\n false, // normalize\n 0, // stride (0 = auto)\n 0, // offset\n );\n return buf;\n}\n\nfunction drawArraysWithTransformFeedback(\n gl: WebGL2RenderingContext,\n tf: WebGLTransformFeedback | null,\n pri