UNPKG

@openpv/simshady

Version:

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

721 lines (694 loc) 28 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/colormaps.ts var colormaps_exports = {}; __export(colormaps_exports, { interpolateThreeColors: () => interpolateThreeColors, interpolateTwoColors: () => interpolateTwoColors, viridis: () => viridis }); function viridis(t) { t = Math.min(Math.max(t, 0), 1); const c0 = [0.2777273272234177, 0.005407344544966578, 0.3340998053353061]; const c1 = [0.1050930431085774, 1.404613529898575, 1.384590162594685]; const c2 = [-0.3308618287255563, 0.214847559468213, 0.09509516302823659]; const c3 = [-4.634230498983486, -5.799100973351585, -19.33244095627987]; const c4 = [6.228269936347081, 14.17993336680509, 56.69055260068105]; const c5 = [4.776384997670288, -13.74514537774601, -65.35303263337234]; const c6 = [-5.435455855934631, 4.645852612178535, 26.3124352495832]; return [ c0[0] + t * (c1[0] + t * (c2[0] + t * (c3[0] + t * (c4[0] + t * (c5[0] + t * c6[0]))))), c0[1] + t * (c1[1] + t * (c2[1] + t * (c3[1] + t * (c4[1] + t * (c5[1] + t * c6[1]))))), c0[2] + t * (c1[2] + t * (c2[2] + t * (c3[2] + t * (c4[2] + t * (c5[2] + t * c6[2]))))) ]; } function interpolateTwoColors(colors) { const { c0, c1 } = colors; return (t) => { t = Math.min(Math.max(t, 0), 1); const r = c0[0] * (1 - t) + c1[0] * t; const g = c0[1] * (1 - t) + c1[1] * t; const b = c0[2] * (1 - t) + c1[2] * t; return [r, g, b]; }; } function interpolateThreeColors(colors) { const { c0, c1, c2 } = colors; return (t) => { t = Math.max(0, Math.min(1, t)); function quadraticInterpolation(t2, v0, v1, v2) { return (1 - t2) * (1 - t2) * v0 + 2 * (1 - t2) * t2 * v1 + t2 * t2 * v2; } const r = quadraticInterpolation(t, c0[0], c1[0], c2[0]); const g = quadraticInterpolation(t, c0[1], c1[1], c2[1]); const b = quadraticInterpolation(t, c0[2], c1[2], c2[2]); return [r, g, b]; }; } // src/main.ts import * as THREE from "three"; import { BufferAttribute, BufferGeometry } from "three"; import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js"; // src/elevation.ts function calculateSphericalCoordinates(start, end) { const dx = end.x - start.x; const dy = end.y - start.y; const dz = end.z - start.z; if (dx == 0 && dy == 0) { return { radius: 1, azimuth: 0, altitude: 0 }; } const r = Math.sqrt(dx * dx + dy * dy + dz * dz); const altitude = Math.asin(dz / r); let azimuth = (2 * Math.PI - Math.atan2(dy, dx)) % (2 * Math.PI); return { radius: 1, azimuth, altitude }; } function getElevationShadingMask(elevation, observer, directions) { const shadingMask = []; for (const [altDeg, azDeg] of directions) { const azRad = azDeg * Math.PI / 180; const altRad = altDeg * Math.PI / 180; let maxAltitude = -Infinity; for (const point of elevation) { const { azimuth, altitude } = calculateSphericalCoordinates(observer, point); const azDiff = Math.abs((azimuth - azRad + Math.PI) % (2 * Math.PI) - Math.PI); if (azDiff < Math.PI / 180) { if (altitude > maxAltitude) maxAltitude = altitude; } } const isVisible = altRad > maxAltitude ? 1 : 0; shadingMask.push([altDeg, azDeg, isVisible]); } return shadingMask; } // src/sun.ts function calculatePVYield(intensities, solarToElectricityConversionEfficiency, totalHours) { const factor = totalHours * 0.065 / 1e3 * solarToElectricityConversionEfficiency; return intensities.map((arr) => new Float32Array(arr.map((x) => x * factor))); } // src/triangleUtils.ts function normalAndArea(positions, startIndex) { const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = positions.slice(startIndex, startIndex + 9); const d01x = x1 - x0; const d01y = y1 - y0; const d01z = z1 - z0; const d02x = x2 - x0; const d02y = y2 - y0; const d02z = z2 - z0; const crsx = d01y * d02z - d01z * d02y; const crsy = d01z * d02x - d01x * d02z; const crsz = d01x * d02y - d01y * d02x; const crs_norm = Math.sqrt(crsx * crsx + crsy * crsy + crsz * crsz); const area = crs_norm / 2; const normal2 = [crsx / crs_norm, crsy / crs_norm, crsz / crs_norm]; return [normal2, area]; } function normal(positions, startIndex) { return normalAndArea(positions, startIndex)[0]; } function subdivide(positions, startIndex, threshold) { const triangle = positions.slice(startIndex, startIndex + 9); const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = triangle; const d01x = x1 - x0; const d01y = y1 - y0; const d01z = z1 - z0; const d02x = x2 - x0; const d02y = y2 - y0; const d02z = z2 - z0; const d12x = x2 - x1; const d12y = y2 - y1; const d12z = z2 - z1; const l01 = d01x * d01x + d01y * d01y + d01z * d01z; const l02 = d02x * d02x + d02y * d02y + d02z * d02z; const l12 = d12x * d12x + d12y * d12y + d12z * d12z; const longest = Math.max(l01, l02, l12); if (longest <= threshold * threshold) { return Array.from(triangle); } if (l01 == longest) { const xm = (x0 + x1) / 2; const ym = (y0 + y1) / 2; const zm = (z0 + z1) / 2; const tri1 = [x0, y0, z0, xm, ym, zm, x2, y2, z2]; const tri2 = [x1, y1, z1, x2, y2, z2, xm, ym, zm]; return subdivide(tri1, 0, threshold).concat(subdivide(tri2, 0, threshold)); } else if (l02 == longest) { const xm = (x0 + x2) / 2; const ym = (y0 + y2) / 2; const zm = (z0 + z2) / 2; const tri1 = [x0, y0, z0, x1, y1, z1, xm, ym, zm]; const tri2 = [x1, y1, z1, x2, y2, z2, xm, ym, zm]; return subdivide(tri1, 0, threshold).concat(subdivide(tri2, 0, threshold)); } else if (l12 == longest) { const xm = (x1 + x2) / 2; const ym = (y1 + y2) / 2; const zm = (z1 + z2) / 2; const tri1 = [x0, y0, z0, x1, y1, z1, xm, ym, zm]; const tri2 = [x2, y2, z2, x0, y0, z0, xm, ym, zm]; return subdivide(tri1, 0, threshold).concat(subdivide(tri2, 0, threshold)); } else { throw new Error("No edge is longest, this shouldn't happen"); } } function midpoint(positions, startIndex) { const [x0, y0, z0, x1, y1, z1, x2, y2, z2] = positions.slice(startIndex, startIndex + 9); return [(x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3, (z0 + z1 + z2) / 3]; } // src/utils.ts async function timeoutForLoop(start, end, body, step = 1) { return new Promise((resolve) => { const inner = (i) => { body(i); i = i + step; if (i >= end) { resolve(); } else { setTimeout(() => inner(i), 0); } }; setTimeout(() => inner(start), 0); }); } function logNaNCount(name, array) { const nanCount = Array.from(array).filter(isNaN).length; if (nanCount > 0) { console.log(`${nanCount}/${array.length} ${name} coordinates are NaN`); } } // src/rayTracingWebGL.ts async function rayTracingWebGL(midpointsArray, normals, trianglesArray, skysegmentDirectionArray, progressCallback) { const N_TRIANGLES = trianglesArray.length / 9; const width = midpointsArray.length / 3; const N_POINTS = width; const gl = document.createElement("canvas").getContext("webgl2"); if (!gl) { throw new Error("Browser does not support WebGL2"); } const vertexShaderSource = `#version 300 es #define INFINITY 1000000.0 precision highp float; uniform sampler2D u_triangles; uniform vec3 u_sun_direction; uniform int textureWidth; in vec3 a_position; in vec3 a_normal; out vec4 outColor; vec3 cross1(vec3 a, vec3 b) { vec3 c = vec3(0, 0, 0); c.x = a[1] * b[2] - a[2] * b[1]; c.y = a[2] * b[0] - a[0] * b[2]; c.z = a[0] * b[1] - a[1] * b[0]; return c; } float TriangleIntersect( vec3 v0, vec3 v1, vec3 v2, vec3 rayOrigin, vec3 rayDirection, int isDoubleSided ) { vec3 edge1 = v1 - v0; vec3 edge2 = v2 - v0; vec3 pvec = cross(rayDirection, edge2); float epsilon = 0.000001; // Add epsilon to avoid division by zero float det = dot(edge1, pvec); if (abs(det) < epsilon) // Check if det is too close to zero return INFINITY; float inv_det = 1.0 / det; if ( isDoubleSided == 0 && det < 0.0 ) return INFINITY; vec3 tvec = rayOrigin - v0; float u = dot(tvec, pvec) * inv_det; vec3 qvec = cross(tvec, edge1); float v = dot(rayDirection, qvec) * inv_det; float t = dot(edge2, qvec) * inv_det; float x = dot(pvec,pvec); return (u < 0.0 || u > 1.0 || v < 0.0 || u + v > 1.0 || t <= 0.01) ? INFINITY : t; } bool Calculate_Shading_at_Point(vec3 vertex_position, vec3 sun_direction) { float d; float t = INFINITY; bool is_shadowed = false; for (int i = 0; i < ${N_TRIANGLES}; i++) { int index = i * 3; int x = index % textureWidth; int y = index / textureWidth; vec3 v0 = texelFetch(u_triangles, ivec2(x, y), 0).rgb; index = i * 3 + 1; x = index % textureWidth; y = index / textureWidth; vec3 v1 = texelFetch(u_triangles, ivec2(x, y), 0).rgb; index = i * 3 + 2; x = index % textureWidth; y = index / textureWidth; vec3 v2 = texelFetch(u_triangles, ivec2(x, y), 0).rgb; d = TriangleIntersect(v0, v1, v2, vertex_position, sun_direction, 1); if (d < t && abs(d)>0.0001) { return true; } } return is_shadowed; } void main() { if (Calculate_Shading_at_Point(a_position.xyz, u_sun_direction)) { outColor = vec4(0, 0, 0, 0); // Shadowed } else { float intensity = abs(dot(a_normal.xyz, u_sun_direction)); outColor = vec4(intensity, intensity, intensity, intensity); // Not shadowed } }`; const fragmentShaderSource = `#version 300 es precision highp float; void main() { } `; const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); const program = createProgram(gl, vertexShader, fragmentShader, ["outColor"]); const vao = gl.createVertexArray(); gl.bindVertexArray(vao); var maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); var textureWidth = Math.min(3 * N_TRIANGLES, Math.floor(maxTextureSize / 9) * 9); var textureHeight = Math.ceil(3 * N_TRIANGLES / textureWidth); const colorBuffer = makeBuffer(gl, N_POINTS * 16); const tf = makeTransformFeedback(gl, colorBuffer); gl.useProgram(program); var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); var alignedTrianglesArray; if (textureHeight == 1) { alignedTrianglesArray = trianglesArray; } else { alignedTrianglesArray = new Float32Array(textureWidth * textureHeight * 3); for (var i = 0; i < 3 * N_TRIANGLES; i++) { var x = 3 * i % textureWidth; var y = Math.floor(3 * i / textureWidth); var index = y * textureWidth + x; for (var j = 0; j < 3; j++) { alignedTrianglesArray[index + j] = trianglesArray[3 * i + j]; } } } gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB32F, textureWidth, textureHeight, 0, gl.RGB, gl.FLOAT, alignedTrianglesArray); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.bindTexture(gl.TEXTURE_2D, null); var u_trianglesLocation = gl.getUniformLocation(program, "u_triangles"); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(u_trianglesLocation, 0); var u_textureWidth = gl.getUniformLocation(program, "textureWidth"); gl.uniform1i(u_textureWidth, textureWidth); const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); const normalAttributeLocation = gl.getAttribLocation(program, "a_normal"); const positionBuffer = makeBufferAndSetAttribute(gl, midpointsArray, positionAttributeLocation); const normalBuffer = makeBufferAndSetAttribute(gl, normals, normalAttributeLocation); var shadedMaskScenes = []; await timeoutForLoop( 0, skysegmentDirectionArray.length, (i2) => { progressCallback(Math.floor(i2 / 3), Math.floor(skysegmentDirectionArray.length / 3)); let x2 = skysegmentDirectionArray[i2]; let y2 = skysegmentDirectionArray[i2 + 1]; let z = skysegmentDirectionArray[i2 + 2]; let magnitude = Math.sqrt(x2 * x2 + y2 * y2 + z * z); if (magnitude < 1e-10) { x2 = 0; y2 = 0; z = 1; } else { x2 = x2 / magnitude; y2 = y2 / magnitude; z = z / magnitude; } let sunDirectionUniformLocation = gl.getUniformLocation(program, "u_sun_direction"); gl.uniform3fv(sunDirectionUniformLocation, [x2, y2, z]); drawArraysWithTransformFeedback(gl, tf, gl.POINTS, N_POINTS); let colorCodedArray = getResults(gl, colorBuffer, N_POINTS); shadedMaskScenes[Math.floor(i2 / 3)] = colorCodedArray.filter((_, index2) => (index2 + 1) % 4 === 0); }, 3 ); gl.deleteTexture(texture); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); gl.deleteProgram(program); gl.deleteBuffer(positionBuffer); gl.deleteBuffer(normalBuffer); gl.deleteTransformFeedback(tf); gl.deleteBuffer(colorBuffer); return shadedMaskScenes; } function getResults(gl, buffer, N_POINTS) { let results = new Float32Array(N_POINTS * 4); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.getBufferSubData( gl.ARRAY_BUFFER, 0, // byte offset into GPU buffer, results ); gl.bindBuffer(gl.ARRAY_BUFFER, null); return results; } function createShader(gl, type, source) { const shader = gl.createShader(type); if (shader === null) { return null; } gl.shaderSource(shader, source); gl.compileShader(shader); const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (success) { return shader; } console.error(gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } function createProgram(gl, vertexShader, fragmentShader, variables_of_interest) { const program = gl.createProgram(); if (program === null || vertexShader === null || fragmentShader === null) { throw new Error("abortSimulation"); } else { gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.transformFeedbackVaryings(program, variables_of_interest, gl.SEPARATE_ATTRIBS); gl.linkProgram(program); const success = gl.getProgramParameter(program, gl.LINK_STATUS); if (success) { return program; } console.error(gl.getProgramInfoLog(program)); gl.deleteProgram(program); } throw new Error("Program compilation error."); } function makeBuffer(gl, sizeOrData) { const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, gl.DYNAMIC_DRAW); return buf; } function makeTransformFeedback(gl, buffer) { const tf = gl.createTransformFeedback(); gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); return tf; } function makeBufferAndSetAttribute(gl, data, loc) { const buf = makeBuffer(gl, data); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer( loc, 3, // size (num components) gl.FLOAT, // type of data in buffer false, // normalize 0, // stride (0 = auto) 0 // offset ); return buf; } function drawArraysWithTransformFeedback(gl, tf, primitiveType, count) { gl.enable(gl.RASTERIZER_DISCARD); gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); gl.beginTransformFeedback(gl.POINTS); gl.drawArrays(primitiveType, 0, count); gl.endTransformFeedback(); gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); gl.disable(gl.RASTERIZER_DISCARD); } // src/main.ts var ShadingScene = class { constructor() { this.elevationRaster = []; this.elevationRasterMidpoint = { x: 0, y: 0, z: 0 }; this.solarIrradiance = null; this.colorMap = viridis; } /** * Adds a geometry as a target for the shading simulation. * For these geometries, the PV potential will be simulated. * This geometry will also be used as a shading geometry, hence * it is not needed to additionally add it by using `addShadingGeometry`. * * @param geometry Flat Buffer Array of a Three.js geometry, where three * consecutive numbers of the array represent one 3D point and nine consecutive * numbers represent one triangle. */ addSimulationGeometry(geometry) { geometry = geometry.toNonIndexed(); if (!this.simulationGeometry) { this.simulationGeometry = geometry; } else { this.simulationGeometry = BufferGeometryUtils.mergeGeometries([this.simulationGeometry, geometry]); } if (!this.shadingGeometry) { this.shadingGeometry = geometry; } else { this.shadingGeometry = BufferGeometryUtils.mergeGeometries([this.shadingGeometry, geometry]); } } /** * Adds a geometry as an outer geometry for the shading simulation. * These geometries are responsible for shading. * * @param geometry Flat Buffer Array of a Three.js geometry, where three * consecutive numbers of the array represent one 3D point and nine consecutive * numbers represent one triangle. */ addShadingGeometry(geometry) { geometry = geometry.toNonIndexed(); if (!this.shadingGeometry) { this.shadingGeometry = geometry; } else { this.shadingGeometry = BufferGeometryUtils.mergeGeometries([this.shadingGeometry, geometry]); } } /** * Add a elevation model to the simulation scene. * @param raster List of Points with x,y,z coordinates, representing a digital elevation model (DEM). It is * important that all values of x,y and z are given with same units. If x and y are given in lat / lon and * z is given in meters, this will result in wrong simulation Results. * @param midpoint The point of the observer, ie the center of the building * angle will be [0, ..., 2Pi] where the list has a lenght of azimuthDivisions */ addElevationRaster(raster, midpoint2) { this.elevationRaster = raster; this.elevationRasterMidpoint = midpoint2; } /** * Add data of solar irradiance to the scene. If it comes as a List of SolarIrradianceData, * this is interpreted as a time series of skydomes. * * **Important Note:** The first skydome of the list is used for the coloring of the final mesh! * Check out the type definition of {@link utils.SolarIrradianceData} for more information. * @param irradiance */ addSolarIrradiance(irradiance) { if (!Array.isArray(irradiance)) { irradiance = [irradiance]; } this.solarIrradiance = irradiance; } /** * Fetches a SolarIrradiance Object from a url and adds it to the * ShadingScene. * @param url */ async addSolarIrradianceFromURL(url) { const response = await fetch(url); const data = await response.json(); this.addSolarIrradiance(data); } /** * Change the Color Map that is used for the colors of the simulated Three.js mesh. This is * optional, the default colorMap is viridis (blue to green to yellow). Other options are * {@link colormaps.interpolateTwoColors} or {@link colormaps.interpolateThreeColors} * @param colorMap */ addColorMap(colorMap) { this.colorMap = colorMap; } /** @ignore * Gets a BufferGeometry representing a mesh. Refines the triangles until all triangles * have sites smaller maxLength. */ refineMesh(mesh, maxLength) { const positions = mesh.attributes.position.array.slice(); let newTriangles = []; let newNormals = []; for (let i = 0; i < positions.length; i += 9) { let normal2 = normal(positions, i); if (normal2[2] < -0.9) { continue; } let triangles = subdivide(positions, i, maxLength); newTriangles = newTriangles.concat(triangles); newNormals = newNormals.concat(triangles.map((_, i2) => normal2[i2 % 3])); } let geometry = new BufferGeometry(); const normalsArray = new Float32Array(newNormals); const positionArray = new Float32Array(newTriangles); geometry.setAttribute("position", new BufferAttribute(positionArray, 3)); geometry.setAttribute("normal", new BufferAttribute(normalsArray, 3)); geometry.attributes.position.needsUpdate = true; geometry.attributes.normal.needsUpdate = true; return geometry; } /** * This function is called as a last step, after the scene is fully build. * It runs the shading simulation and returns a THREE.js colored mesh. * The colors are chosen from the defined colorMap. * @param params The input object containing information about the simulation. * @returns A three.js colored mesh of the simulationGeometry. Each triangle gets an * attribute called intensity, that holds the annual electricity in kwh/m2 that a PV * system can generate. If {@link ShadingScene.solarIrradiance} is a timeseries of sky * domes, the resulting intensities attribute is a flattened Float32Array of length T*N. */ async calculate(params = {}) { const { solarToElectricityConversionEfficiency = 0.15, maxYieldPerSquareMeter = 1400 * 0.15, progressCallback = (progress, total) => console.log(`Progress: ${progress}/${total}%`) } = params; if (!this.validateClassParams()) { throw new Error( "Invalid Class Parameters: You need to supply at least Shading Geometry, a Simulation Geometry, and Irradiance Data." ); } this.simulationGeometry = this.refineMesh(this.simulationGeometry, 1); const meshArray = this.shadingGeometry.attributes.position.array; const points = this.simulationGeometry.attributes.position.array; const normalsArray = this.simulationGeometry.attributes.normal.array.filter((_, index) => index % 9 < 3); const midpointsArray = this.computeMidpoints(points); logNaNCount("midpoints", midpointsArray); logNaNCount("mesh", meshArray); const shadedScene = await this.rayTrace( midpointsArray, normalsArray, meshArray, this.solarIrradiance, // Non-null assertion (i, total) => progressCallback(i + total, total) ); const pvYield = calculatePVYield( shadedScene, solarToElectricityConversionEfficiency, this.solarIrradiance[0].metadata.valid_timesteps_for_aggregation ); return this.createMesh(this.simulationGeometry, pvYield, maxYieldPerSquareMeter); } // Type Guard function to validate class parameters validateClassParams() { return this.shadingGeometry !== null && this.shadingGeometry !== void 0 && this.simulationGeometry !== null && this.simulationGeometry !== void 0 && this.solarIrradiance != null; } // Helper to compute midpoints of triangles and track NaN values computeMidpoints(points) { let midpoints = []; for (let i = 0; i < points.length; i += 9) { const midpoint2 = midpoint(points, i); midpoints.push(...midpoint2); } return new Float32Array(midpoints); } /** * @ignore * This function does two things: * - it assigns a color to the given simulationGeometry. The color is assigned * using the FIRST value of the intensities time series and the maxYieldPerSquareMeter * as upper boundary. * - it flattens the time series of intensities and sets them as attribute to the simulationGeometry * * @param simulationGeometry Nx9 Array with the edge points of N triangles * @param intensities T x N intensities, one for every triangle and every time step * @param maxYieldPerSquareMeter number defining the upper boundary of the color map * @returns Mesh with color and new attribute "intensities" that has length T*N */ createMesh(simulationGeometry, intensities, maxYieldPerSquareMeter) { const Npoints = simulationGeometry.attributes.position.array.length / 9; var newColors = new Float32Array(Npoints * 9); for (var i = 0; i < Npoints; i++) { const col = this.colorMap(Math.min(maxYieldPerSquareMeter, intensities[0][i]) / maxYieldPerSquareMeter); for (let j = 0; j < 9; j += 3) { newColors[9 * i + j] = col[0]; newColors[9 * i + j + 1] = col[1]; newColors[9 * i + j + 2] = col[2]; } } simulationGeometry.setAttribute("color", new THREE.Float32BufferAttribute(newColors, 3)); var material = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide }); const flatIntensities = new Float32Array(intensities.map((arr) => Array.from(arr)).flat()); simulationGeometry.setAttribute("intensities", new THREE.Float32BufferAttribute(flatIntensities, 1)); let mesh = new THREE.Mesh(simulationGeometry, material); return mesh; } /** @ignore * This function returns a time series of intensities of shape T x N, with N the number of midpoints. * It includes the shading of geometries, the dot product of normal vector and sky segment vector, * and the radiation values from diffuse and direct irradiance. * * @param midpoints midpoints of triangles for which to calculate intensities * @param normals normals for each midpoint * @param meshArray array of vertices for the shading mesh * @param irradiance Time Series of sky domes * @return */ async rayTrace(midpoints, normals, meshArray, irradiance, progressCallback) { function convertSolarIrradianceToFloat32Array(solarIrradiance) { const directions = []; const radiation = []; for (const entry of solarIrradiance) { const radiances = []; for (const point of entry.data) { const altRad = point.altitude_deg * Math.PI / 180; const azRad = point.azimuth_deg * Math.PI / 180; const x = Math.cos(altRad) * Math.sin(azRad); const y = Math.cos(altRad) * Math.cos(azRad); const z = Math.sin(altRad); directions.push(x, y, z); radiances.push(point.average_radiance_W_m2_sr); } radiation.push(new Float32Array(radiances)); } return { skysegmentDirections: new Float32Array(directions), skysegmentRadiation: radiation }; } const { skysegmentDirections, skysegmentRadiation } = convertSolarIrradianceToFloat32Array(irradiance); const shadedMaskScenes = await rayTracingWebGL(midpoints, normals, meshArray, skysegmentDirections, progressCallback); if (shadedMaskScenes === null) { throw new Error("Error occured when running the Raytracing in WebGL."); } if (this.elevationRaster.length > 0) { const elevationShadingMask = getElevationShadingMask( this.elevationRaster, this.elevationRasterMidpoint, // extract the altitude azimuth pairs from the first skysegment irradiance[0].data.map(({ altitude_deg, azimuth_deg }) => [altitude_deg, azimuth_deg]) ); } let intensities = skysegmentRadiation.map(() => new Float32Array(midpoints.length / 3)); for (let i = 0; i < shadedMaskScenes.length; i++) { for (let j = 0; j < midpoints.length; j++) { for (let t = 0; t < intensities.length; t++) { intensities[t][j] += shadedMaskScenes[i][j] * skysegmentRadiation[t][i]; } } } return intensities; } }; export { ShadingScene, colormaps_exports as colormaps }; //# sourceMappingURL=index.js.map