@openpv/simshady
Version:
Simulating Shadows for PV Potential Analysis on 3D Data on the GPU.
721 lines (694 loc) • 28 kB
JavaScript
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