UNPKG

@openpv/simshady

Version:

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

909 lines (880 loc) 35.3 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 as BufferGeometry2 } 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 result = []; const stack = []; const initialTriangle = Array.from(positions.slice(startIndex, startIndex + 9)); stack.push(initialTriangle); while (stack.length > 0) { const triangle = stack.pop(); 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) { result.push(...triangle); continue; } 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]; stack.push(tri1, tri2); } 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]; stack.push(tri1, tri2); } 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]; stack.push(tri1, tri2); } else { throw new Error("No edge is longest, this shouldn't happen"); } } return result; } 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; uniform int u_triangleStart; uniform int u_triangleCount; 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++) { if (i >= u_triangleCount) { break; } int tri = u_triangleStart + i; int index = tri * 3; int x = index % textureWidth; int y = index / textureWidth; vec3 v0 = texelFetch(u_triangles, ivec2(x, y), 0).rgb; index = tri * 3 + 1; x = index % textureWidth; y = index / textureWidth; vec3 v1 = texelFetch(u_triangles, ivec2(x, y), 0).rgb; index = tri * 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, maxTextureSize); 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 = []; const MAX_TRIANGLES_PER_PASS = 4096; const passCount = Math.max(1, Math.ceil(N_TRIANGLES / MAX_TRIANGLES_PER_PASS)); const u_triangleStartLocation = gl.getUniformLocation(program, "u_triangleStart"); const u_triangleCountLocation = gl.getUniformLocation(program, "u_triangleCount"); 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]); let shadedMaskScene = null; for (let pass = 0; pass < passCount; pass++) { const start = pass * MAX_TRIANGLES_PER_PASS; const count = Math.min(MAX_TRIANGLES_PER_PASS, N_TRIANGLES - start); gl.uniform1i(u_triangleStartLocation, start); gl.uniform1i(u_triangleCountLocation, count); drawArraysWithTransformFeedback(gl, tf, gl.POINTS, N_POINTS); let colorCodedArray = getResults(gl, colorBuffer, N_POINTS); let intensities = colorCodedArray.filter((_, index2) => (index2 + 1) % 4 === 0); if (shadedMaskScene === null) { shadedMaskScene = intensities; } else { for (let k = 0; k < shadedMaskScene.length; k++) { shadedMaskScene[k] = Math.min(shadedMaskScene[k], intensities[k]); } } } shadedMaskScenes[Math.floor(i2 / 3)] = shadedMaskScene; }, 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/geometryFilter.ts import { Float32BufferAttribute } from "three"; function getMinSunAngleFromIrradiance(irradiance) { const irradianceArray = Array.isArray(irradiance) ? irradiance : [irradiance]; let minAltitude = Infinity; for (const entry of irradianceArray) { for (const point of entry.data) { if (point.average_radiance_W_m2_sr > 0) { minAltitude = Math.min(minAltitude, point.altitude_deg); } } } return minAltitude === Infinity ? 0 : minAltitude; } function calculateBoundingBox(positions) { if (positions.length === 0) { return { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } }; } let minX = Infinity, minY = Infinity, minZ = Infinity; let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; const z = positions[i + 2]; minX = Math.min(minX, x); minY = Math.min(minY, y); minZ = Math.min(minZ, z); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); maxZ = Math.max(maxZ, z); } return { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } }; } function calculateMinimumHeight(horizontalDistance, groundLevel, minSunAngle) { const angleRadians = minSunAngle * Math.PI / 180; return groundLevel + horizontalDistance * Math.tan(angleRadians); } function getTriangleHorizontalDistance(triangleVertices, boundingBox) { let minDistance = Infinity; for (let i = 0; i < 9; i += 3) { const x = triangleVertices[i]; const y = triangleVertices[i + 1]; let dx = 0; let dy = 0; if (x < boundingBox.min.x) { dx = boundingBox.min.x - x; } else if (x > boundingBox.max.x) { dx = x - boundingBox.max.x; } if (y < boundingBox.min.y) { dy = boundingBox.min.y - y; } else if (y > boundingBox.max.y) { dy = y - boundingBox.max.y; } const distance = Math.sqrt(dx * dx + dy * dy); minDistance = Math.min(minDistance, distance); } return minDistance; } function shouldKeepTriangle(triangleVertices, boundingBox, groundLevel, minSunAngle) { const z1 = triangleVertices[2]; const z2 = triangleVertices[5]; const z3 = triangleVertices[8]; const maxZ = Math.max(z1, z2, z3); const horizontalDistance = getTriangleHorizontalDistance(triangleVertices, boundingBox); const minHeight = calculateMinimumHeight(horizontalDistance, groundLevel, minSunAngle); return maxZ >= minHeight; } function filterShadingGeometry(simPos, shadePos, minSunAngle, silent = false) { if (shadePos.length === 0) { if (!silent) console.log("No shading geometry to filter, skipping filtering"); return shadePos; } if (simPos.length === 0) { if (!silent) console.log("No simulation geometry provided, skipping filtering"); return shadePos; } if (minSunAngle <= 0) { if (!silent) console.log("Min sun angle <= 0, skipping filtering"); return shadePos; } const boundingBox = calculateBoundingBox(simPos); const groundLevel = boundingBox.min.z; const totalTriangles = Math.floor(shadePos.length / 9); const keptTriangles = []; for (let i = 0; i < shadePos.length; i += 9) { const triangleVertices = [ shadePos[i], shadePos[i + 1], shadePos[i + 2], shadePos[i + 3], shadePos[i + 4], shadePos[i + 5], shadePos[i + 6], shadePos[i + 7], shadePos[i + 8] ]; if (shouldKeepTriangle(triangleVertices, boundingBox, groundLevel, minSunAngle)) { for (let j = 0; j < 9; j++) { keptTriangles.push(triangleVertices[j]); } } } const filteredArray = new Float32Array(keptTriangles); if (!silent) { const percentKept = (keptTriangles.length / 9 / totalTriangles * 100).toFixed(2); console.log(`Shading geometry filtering:`); console.log(`Min. sun angle: ${minSunAngle}\xB0`); console.log(`Total triangles: ${totalTriangles}`); console.log(`Kept triangles: ${keptTriangles.length / 9}`); console.log(`Percentage kept: ${percentKept}%`); } return filteredArray; } function filterShadingBufferGeometry(simulationGeometry, shadingGeometry, minSunAngle) { const simPos = new Float32Array(simulationGeometry == null ? void 0 : simulationGeometry.getAttribute("position").array); const shadePos = new Float32Array(shadingGeometry == null ? void 0 : shadingGeometry.getAttribute("position").array); const filteredPos = filterShadingGeometry(simPos, shadePos, minSunAngle); shadingGeometry.setAttribute("position", new Float32BufferAttribute(filteredPos, 3)); return shadingGeometry; } // 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 [BufferGeometry](https://threejs.org/docs/#api/en/core/BufferGeometry) 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 [BufferGeometry](https://threejs.org/docs/#api/en/core/BufferGeometry) of a Three.js geometry, where three * consecutive numbers of the array represent one 3D point and nine consecutive * numbers represent one triangle. * @param minSunAngle The minimum radiance angle which gets used during raytracing. It is being used for filtering out * shading geometry which physically cannot shade the simulation geometry. If none is provided the min. angle of the * provided irradiance data will be used. */ addShadingGeometry(geometry, minSunAngle) { if (minSunAngle !== void 0) { this.minSunAngle = minSunAngle; } 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(); const newTriangles = []; const newNormals = []; for (let i = 0; i < positions.length; i += 9) { const normal2 = normal(positions, i); if (normal2[2] < -0.9) { continue; } const triangles = subdivide(positions, i, maxLength); for (let j = 0; j < triangles.length; j++) { newTriangles.push(triangles[j]); newNormals.push(normal2[j % 3]); } } const geometry = new BufferGeometry2(); 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 = {}) { var _a; const { solarToElectricityConversionEfficiency = 0.15, maxYieldPerSquareMeter = 1400 * 0.15, progressCallback = (progress, total, elapsed, remaining) => { const format = (s) => { const min = Math.floor(s / 60); const sec = Math.floor(s % 60); return min > 0 ? `${min}m ${sec}s` : `${sec}s`; }; console.log(`Progress: ${progress}/${total} | Elapsed: ${format(elapsed)} | Est. remaining: ${format(remaining)}`); } } = params; if (!this.validateClassParams()) { throw new Error( "Invalid Class Parameters: You need to supply at least Shading Geometry, a Simulation Geometry, and Irradiance Data." ); } const minSunAngle = (_a = this.minSunAngle) != null ? _a : getMinSunAngleFromIrradiance(this.solarIrradiance); this.shadingGeometry = filterShadingBufferGeometry(this.simulationGeometry, this.shadingGeometry, minSunAngle); 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 startTime = Date.now(); const wrappedCallback = (progress, total) => { if (progress === 0) { return; } const elapsed = (Date.now() - startTime) / 1e3; const average = elapsed / progress; const remaining = Math.max(0, (total - progress) * average); progressCallback(progress, total, elapsed, remaining); }; const shadedScene = await this.rayTrace( midpointsArray, normalsArray, meshArray, this.solarIrradiance, // Non-null assertion wrappedCallback ); 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