import * as Cesium from "cesium";
import {ComputePrimitive} from "./ComputePrimitive";
import {RenderPrimitive} from "./RenderPrimitive";
import {LonLatAltVolume} from "./Volume";
import {ShaderLoader} from "@/modules/ShaderLoader.js";
/**
* MagoWind is a class that creates a wind effect.
* @class
* @param {Cesium.Viewer} viewer - Cesium Viewer instance
* @example
* const windOptions = {
* dimension: dimension, // grid count [x,y]
* levels: levels, // grid altitude [ (altitude of grid[0]), ... ]
* uvws: uvws, // uvw for each grid [ { u:[...], v:[...], w:[...] }, ... ]
* boundary: boundary, // lon lat boundary for grid [ [lon, lat](leftlow), (rightlow), (righthigh), (lefthigh) ]
* textureSize: 512, // particle count = (textureSize * textureSize)
* speedFactor: 500.0, // particle speed factor
* renderingType: 'triangle', // Rendering Type (one of 'point', 'line', 'triangle')
* point: {
* pointSize: 2,
* },
* triangle: {
* lineWidth: 1000,
* }
* }
* const magoWind = new MagoWind(viewer);
* magoWind.init(options);
*/
export class MagoWind {
/**
* Minimum trail length
* @type {number}
*/
static MIN_TRAIL_LENGTH = 2;
/**
* Maximum trail length
* @type {number}
*/
static MAX_TRAIL_LENGTH = 10;
/**
* Creates an instance of MagoWind.
* @param viewer
*/
constructor(viewer, baseUrl = "/") {
console.log("[MCT][WIND] constructor");
/**
* Cesium Viewer instance
*/
this.viewer = viewer;
/**
* Base URL for loading resources
* @type {string}
*/
this.baseUrl = baseUrl.replace(/\/$/, "");
this.context = viewer.scene.context;
this.shaderLoader = new ShaderLoader("/src/customShaders/wind");
}
/**
* Initializes the wind effect.
* @function
* @param options
* @returns {Promise<void>}
*/
async init(options) {
console.log("[MCT][WIND] initialize");
Object.assign(this, options);
const viewer = this.viewer;
const context = this.context;
const calculateWindPosition = await this.shaderLoader.getShaderSource("calculateWindPosition.frag");
const calculateWindColor = await this.shaderLoader.getShaderSource("calculateWindColor.frag");
const normalized2ecef = await this.shaderLoader.getShaderSource("normalized2ecef.frag");
const fullscreenVertexShader = await this.shaderLoader.getShaderSource("fullscreen.vert");
const screenDrawFragmentShader = await this.shaderLoader.getShaderSource("screenDraw.frag");
// output primitive collection
const collection = this.primitiveCollection = new Cesium.PrimitiveCollection();
// input data
const {
dimension, levels, uvws, boundary, textureSize, speedFactor, trailLength, renderingType,
} = Object.assign({
/* default options */
textureSize: 1000, speedFactor: 1000.0, renderingType: "triangle", // trailLength: 5,
point: {
size: 2,
}, triangle: {
lineWidth: 1000.0,
},
}, options, {
trailLength: options.trailLength ? Math.max(MagoWind.MIN_TRAIL_LENGTH, Math.min(MagoWind.MAX_TRAIL_LENGTH, options.trailLength)) : 5, // position buffer length (for position history)
});
const valueMinMax = [
[
Math.min(...uvws.u.map(grid => grid.reduce((prev, curr) => prev > curr ? curr : prev))), Math.max(...uvws.u.map(grid => grid.reduce((prev, curr) => prev < curr ? curr : prev)))], [
Math.min(...uvws.v.map(grid => grid.reduce((prev, curr) => prev > curr ? curr : prev))), Math.max(...uvws.v.map(grid => grid.reduce((prev, curr) => prev < curr ? curr : prev)))], [
Math.min(...uvws.w.map(grid => grid.reduce((prev, curr) => prev > curr ? curr : prev))), Math.max(...uvws.w.map(grid => grid.reduce((prev, curr) => prev < curr ? curr : prev)))]];
const volume = new LonLatAltVolume(boundary, [levels[0], levels[levels.length - 1]]);
// convert data into normalized, combined wind speed texture
const combinedUVWs = []; // 모든 레벨을 하나의 texture로 합침
levels.forEach((level, levelIndex) => {
// normalize 해서 텍스쳐로 변환
const UVW = combinedUVWs;
for (let j = 0; j < dimension[1]; j++) {
for (let i = 0; i < dimension[0]; i++) {
UVW.push((valueMinMax[0][1] - valueMinMax[0][0]) ? (uvws.u[levelIndex][i + j * dimension[0]] - valueMinMax[0][0]) / (valueMinMax[0][1] - valueMinMax[0][0]) : 0.0);
UVW.push((valueMinMax[1][1] - valueMinMax[1][0]) ? (uvws.v[levelIndex][i + j * dimension[0]] - valueMinMax[1][0]) / (valueMinMax[1][1] - valueMinMax[1][0]) : 0.0);
UVW.push((valueMinMax[2][1] - valueMinMax[2][0]) ? (uvws.w[levelIndex][i + j * dimension[0]] - valueMinMax[2][0]) / (valueMinMax[2][1] - valueMinMax[2][0]) : 0.0);
UVW.push(1.0);
}
}
});
const combinedWindSpeedTextures = this.createTexture({
context: context,
width: dimension[0],
height: dimension[1] * levels.length,
pixelFormat: Cesium.PixelFormat.RGBA,
pixelDatatype: Cesium.PixelDatatype.FLOAT,
flipY: false,
sampler: new Cesium.Sampler({
// the values of texture will not be interpolated
minificationFilter: Cesium.TextureMinificationFilter.NEAREST, // LINEAR 로 자동 보간
magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST, // LINEAR 로 자동 보간
}),
}, new Float32Array(combinedUVWs));
// wind speed & position
const windPositionTextures = new Array(trailLength).fill(0).
map(() => this.createTexture({
context: context, width: textureSize, height: textureSize, pixelFormat: Cesium.PixelFormat.RGBA, pixelDatatype: Cesium.PixelDatatype.FLOAT, flipY: false, sampler: new Cesium.Sampler({
// the values of texture will not be interpolated
minificationFilter: Cesium.TextureMinificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
}),
}, new Float32Array(new Array(textureSize * textureSize * 4).fill(0), // .map((e, i) => Math.random())
)));
const windPosition = new ComputePrimitive({
fragmentShaderSource: new Cesium.ShaderSource({
sources: [calculateWindPosition],
}), uniformMap: {
windPositionTexture: function() {
return windPositionTextures[1]; // current position
}, windSpeedTextures: function() {
return combinedWindSpeedTextures;
}, dimensions: function() {
return new Cesium.Cartesian3(dimension[0], dimension[1], levels.length);
}, altitudes: function() {
return levels;
}, minValues: function() {
return new Cesium.Cartesian3(valueMinMax[0][0], valueMinMax[1][0], valueMinMax[2][0]);
}, maxValues: function() {
return new Cesium.Cartesian3(valueMinMax[0][1], valueMinMax[1][1], valueMinMax[2][1]);
}, bounds: function() {
return volume.bounds.map(v => new Cesium.Cartesian3(v[0], v[1], v[2])).
slice(0, 4);
}, altitudeBounds: function() {
return volume.getAltitudeRange();
}, speedFactor: function() {
return speedFactor;
}, randomParam: function() {
return Math.random();
},
}, outputTexture: windPositionTextures[0], // next position
preExecute: function(primitive) {
// swap textures before binding
windPositionTextures.unshift(windPositionTextures.pop());
// keep the outputTexture up to date
primitive.commandToExecute.outputTexture = windPositionTextures[0];
},
});
// wind color
const windColorTexture = this.createTexture({
context: context, width: textureSize, height: textureSize, pixelFormat: Cesium.PixelFormat.RGBA, pixelDatatype: Cesium.PixelDatatype.FLOAT, flipY: false, sampler: new Cesium.Sampler({
// the values of texture will not be interpolated
minificationFilter: Cesium.TextureMinificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
}),
}, new Float32Array(new Array(textureSize * textureSize * 4).fill(0), // .map((e, i) => Math.random())
));
const windColor = new ComputePrimitive({
fragmentShaderSource: new Cesium.ShaderSource({
sources: [calculateWindColor],
}), uniformMap: {
windPositionTexture: function() {
return windPositionTextures[0];
}, windSpeedTextures: function() {
return combinedWindSpeedTextures;
}, dimensions: function() {
return new Cesium.Cartesian3(dimension[0], dimension[1], levels.length);
}, altitudes: function() {
return levels;
}, minValues: function() {
return new Cesium.Cartesian3(valueMinMax[0][0], valueMinMax[1][0], valueMinMax[2][0]);
}, maxValues: function() {
return new Cesium.Cartesian3(valueMinMax[0][1], valueMinMax[1][1], valueMinMax[2][1]);
}, bounds: function() {
return volume.bounds.map(v => new Cesium.Cartesian3(v[0], v[1], v[2])).
slice(0, 4);
}, altitudeBounds: function() {
return volume.getAltitudeRange();
},
}, outputTexture: windColorTexture,
});
// normalized position => ecef
const ecefPositionTextures = new Array(trailLength).fill(0).
map(() => this.createTexture({
context: context, width: textureSize, height: textureSize, pixelFormat: Cesium.PixelFormat.RGBA, pixelDatatype: Cesium.PixelDatatype.FLOAT, flipY: false, sampler: new Cesium.Sampler({
// the values of texture will not be interpolated
minificationFilter: Cesium.TextureMinificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST, // => LINEAR 로 사용 대체 확인 필요
}),
}, new Float32Array(new Array(textureSize * textureSize * 4).fill(0), // .map((e, i) => Math.random())
)));
const normalized2ECEFs = new Array(trailLength).fill(0).
map((v, i) => new ComputePrimitive({
fragmentShaderSource: new Cesium.ShaderSource({
sources: [normalized2ecef],
}), uniformMap: {
windPositionTexture: function() {
return windPositionTextures[i];
}, bounds: function() {
return volume.bounds.map(v => new Cesium.Cartesian3(v[0], v[1], v[2])).
slice(0, 5);
}, altitudeBounds: function() {
return volume.getAltitudeRange();
},
}, outputTexture: ecefPositionTextures[i],
}));
// projected
const projectedTexture = this.createTexture({
context: context, width: context.drawingBufferWidth, height: context.drawingBufferHeight, pixelFormat: Cesium.PixelFormat.RGBA, pixelDatatype: Cesium.PixelDatatype.FLOAT, // flipY: false,
sampler: new Cesium.Sampler({
// the values of texture will not be interpolated
minificationFilter: Cesium.TextureMinificationFilter.LINEAR, // => LINEAR 로 사용 대체 확인 필요
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR, // => LINEAR 로 사용 대체 확인 필요
}),
}, new Float32Array(new Array(context.drawingBufferWidth * context.drawingBufferHeight * 4).fill(0), // .map((e, i) => { return Math.random() })
));
const projectedDepth = this.createTexture({
context: context, width: context.drawingBufferWidth, height: context.drawingBufferHeight, pixelFormat: Cesium.PixelFormat.DEPTH_COMPONENT, pixelDatatype: Cesium.PixelDatatype.UNSIGNED_INT,
});
const ecefToProjected = (renderingType === "point") ?
await this.createRenderingPoint(context, ecefPositionTextures[0], windColorTexture, projectedTexture, projectedDepth) :
(renderingType === "line") ?
await this.createRenderingLine(context, ecefPositionTextures, windColorTexture, projectedTexture, projectedDepth) :
(renderingType === "triangle") ?
await this.createRenderingTriangle(viewer, context, ecefPositionTextures, windColorTexture, projectedTexture, projectedDepth) :
await this.createRenderingPoint(context, ecefPositionTextures[0], windColorTexture, projectedTexture, projectedDepth);
// screen
const screen = new RenderPrimitive(context, {
attributeLocations: {
position: 0, st: 1,
}, geometry: this.getFullscreenQuad(), primitiveType: Cesium.PrimitiveType.TRIANGLES, uniformMap: {
projectedPosition: function() {
return projectedTexture;
}, projectedDepth: function() {
return projectedDepth;
},
}, vertexShaderSource: new Cesium.ShaderSource({
sources: [fullscreenVertexShader],
}), fragmentShaderSource: new Cesium.ShaderSource({
sources: [screenDrawFragmentShader],
}), rawRenderState: this.createRawRenderState({
// viewport: undefined,
depthTest: {
enabled: true,
}, depthMask: false, blending: {
enabled: true,
},
}), framebuffer: undefined, // undefined value means let Cesium deal with it
});
collection.add(windPosition);
collection.add(windColor);
normalized2ECEFs.forEach((normalized2ECEF) => collection.add(normalized2ECEF));
collection.add(ecefToProjected);
collection.add(screen);
}
/**
* Returns the primitive collection.
* @returns {Promise<module:cesium.PrimitiveCollection>}
*/
async getPrimitiveCollection() {
return await this.primitiveCollection;
}
/**
* Creates a texture.
* @param options
* @param typedArray
* @returns {Cesium.Texture}
*/
createTexture(options, typedArray) {
// console.log('createTexture', options, typedArray)
if (Cesium.defined(typedArray)) {
// typed array needs to be passed as source option, this is required by Cesium.Texture
const source = {};
source.arrayBufferView = typedArray;
options.source = source;
}
return new Cesium.Texture(options);
}
getFullscreenQuad() {
return new Cesium.Geometry({
attributes: new Cesium.GeometryAttributes({
position: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 3, // v3----v2
// | |
// | |
// v0----v1
values: new Float32Array([
-1, -1, 0, // v0
1, -1, 0, // v1
1, 1, 0, // v2
-1, 1, 0, // v3
]),
}), st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 2, values: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
}),
}), indices: new Uint32Array([3, 2, 0, 0, 2, 1]),
});
}
createFramebuffer(context, colorTexture, depthTexture) {
return new Cesium.Framebuffer({
context: context, colorTextures: [colorTexture], depthTexture: depthTexture,
});
}
createRawRenderState(options) {
const translucent = true;
const closed = false;
const existing = {
viewport: options.viewport, depthTest: options.depthTest, depthMask: options.depthMask, blending: options.blending,
};
return Cesium.Appearance.getDefaultRenderState(translucent, closed, existing);
}
createPointCloudGeometry(texture) {
let st = [];
for (let s = 0; s < texture.width; s++) {
for (let t = 0; t < texture.height; t++) {
st.push(s / texture.width);
st.push(t / texture.height);
}
}
st = new Float32Array(st);
return new Cesium.Geometry({
attributes: new Cesium.GeometryAttributes({
st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 2, values: st,
}),
}),
});
}
async createRenderingPoint(context, positionTexture, colorTexture, projectedTexture, projectedDepth) {
const vertexShader_ecef2projected_point = await this.shaderLoader.getShaderSource("ecef2projectedPoint.vert");
const fragmentShader_ecef2projected_point = await this.shaderLoader.getShaderSource("ecef2projectedPoint.frag");
const that = this;
return new RenderPrimitive(context, {
attributeLocations: {
st: 1,
}, geometry: this.createPointCloudGeometry(positionTexture), primitiveType: Cesium.PrimitiveType.POINTS, uniformMap: {
color: function() {
return colorTexture;
}, ecefPosition: function() {
return positionTexture;
}, pointSize: function() {
return that.point?.pointSize || 2;
},
}, vertexShaderSource: new Cesium.ShaderSource({
sources: [vertexShader_ecef2projected_point],
}), fragmentShaderSource: new Cesium.ShaderSource({
sources: [fragmentShader_ecef2projected_point],
}), rawRenderState: this.createRawRenderState({
// viewport: undefined,
depthTest: {
enabled: true,
}, depthMask: true, blending: {
enabled: true,
},
}), framebuffer: this.createFramebuffer(context, projectedTexture, projectedDepth), autoClear: true,
});
}
async createRenderingLine(context, trailTextures, colorTexture, projectedTexture, projectedDepth) {
const vertexShader_ecef2projected_line = await this.shaderLoader.getShaderSource("ecef2projectedLine.vert");
const fragmentShader_ecef2projected_line = await this.shaderLoader.getShaderSource("ecef2projectedLine.frag");
return new RenderPrimitive(context, {
attributeLocations: {
st: 1, normal: 2,
}, geometry: this.createLineStringGeometry(trailTextures), primitiveType: Cesium.PrimitiveType.LINES, uniformMap: {
trailLength: function() {
return trailTextures.length;
}, trailECEFPositionTextures: function() {
return trailTextures;
}, color: function() {
return colorTexture;
},
}, vertexShaderSource: new Cesium.ShaderSource({
sources: [vertexShader_ecef2projected_line],
}), fragmentShaderSource: new Cesium.ShaderSource({
sources: [fragmentShader_ecef2projected_line],
}), rawRenderState: this.createRawRenderState({
// viewport: undefined,
depthTest: {
enabled: true,
}, depthMask: true, blending: {
enabled: true,
},
}), framebuffer: this.createFramebuffer(context, projectedTexture, projectedDepth), autoClear: true,
});
}
async createRenderingTriangle(viewer, context, trailTextures, colorTexture, projectedTexture, projectedDepth) {
const vertexShader_ecef2projected_triangle = await this.shaderLoader.getShaderSource("ecef2projectedTriangle.vert");
const fragmentShader_ecef2projected_triangle = await this.shaderLoader.getShaderSource("ecef2projectedTriangle.frag");
const that = this;
return new RenderPrimitive(context, {
attributeLocations: {
st: 1, normal: 2,
}, geometry: this.createTriangleGeometry(trailTextures), primitiveType: Cesium.PrimitiveType.TRIANGLES, uniformMap: {
trailLength: function() {
return trailTextures.length;
}, trailECEFPositionTextures: function() {
return trailTextures;
}, color: function() {
return colorTexture;
}, cameraPosition: function() {
console.log(viewer.scene.camera.positionWC);
return viewer.scene.camera.positionWC;
}, lineWidth: function() {
return that.triangle?.lineWidth || 1000.0;
},
}, vertexShaderSource: new Cesium.ShaderSource({
sources: [vertexShader_ecef2projected_triangle],
}), fragmentShaderSource: new Cesium.ShaderSource({
sources: [fragmentShader_ecef2projected_triangle],
}), rawRenderState: this.createRawRenderState({
// viewport: undefined,
depthTest: {
enabled: true,
}, depthMask: true, blending: {
enabled: true,
},
}), framebuffer: this.createFramebuffer(context, projectedTexture, projectedDepth), autoClear: true,
});
}
createLineStringGeometry(textures) {
const lineLength = textures.length;
const texture = textures[0];
let st = [];
{
for (let s = 0; s < texture.width; s++) {
for (let t = 0; t < texture.height; t++) {
for (let i = 0; i < lineLength; i++) {
st.push(s / texture.width);
st.push(t / texture.height);
}
}
}
st = new Float32Array(st);
}
let normal = [];
{
for (let s = 0; s < texture.width; s++) {
for (let t = 0; t < texture.height; t++) {
for (let i = 0; i < lineLength; i++) {
normal.push(i); // trail index
normal.push(0); // trail order
normal.push(0); // unused
}
}
}
normal = new Float32Array(normal);
}
const indexSize = (lineLength - 1) * texture.width * texture.height * 2;
let vIndex = 0;
const vertexIndexes = new Uint32Array(indexSize);
{
for (let particleIndex = 0; particleIndex < texture.width * texture.height; particleIndex++) {
// for each particle,
const startVertexIndex = particleIndex * (lineLength);
for (let j = 0; j < lineLength - 1; j++) {
vertexIndexes[vIndex++] = startVertexIndex + j;
vertexIndexes[vIndex++] = startVertexIndex + j + 1;
}
}
}
return new Cesium.Geometry({
attributes: new Cesium.GeometryAttributes({
st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 2, values: st,
}), normal: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 3, values: normal,
}),
}), indices: vertexIndexes,
});
}
createTriangleGeometry(textures) {
const lineLength = textures.length;
const texture = textures[0];
let st = [];
{
for (let s = 0; s < texture.width; s++) {
for (let t = 0; t < texture.height; t++) {
for (let i = 0; i < lineLength; i++) {
// for each line, use 2 vertex
st.push(s / texture.width);
st.push(t / texture.height);
st.push(s / texture.width);
st.push(t / texture.height);
}
}
}
st = new Float32Array(st);
}
let normal = [];
{
for (let s = 0; s < texture.width; s++) {
for (let t = 0; t < texture.height; t++) {
for (let i = 0; i < lineLength; i++) {
normal.push(i); // trail index
normal.push(1); // vertex index
normal.push(0); // unused
normal.push(i); // trail index
normal.push(-1); // vertex index
normal.push(0); // unused
}
}
}
normal = new Float32Array(normal);
}
const indexSize = (lineLength - 1) * texture.width * texture.height * 3 * 2; // line 하나당 triangle 2개씩 필요
let vIndex = 0;
const vertexIndexes = new Uint32Array(indexSize);
{
for (let particleIndex = 0; particleIndex < texture.width * texture.height; particleIndex++) {
const startVertexIndex = particleIndex * (lineLength) * 2;
for (let j = 0; j < lineLength - 1; j++) {
vertexIndexes[vIndex++] = startVertexIndex + j;
vertexIndexes[vIndex++] = startVertexIndex + j + 1;
vertexIndexes[vIndex++] = startVertexIndex + j + 2;
vertexIndexes[vIndex++] = startVertexIndex + j + 2;
vertexIndexes[vIndex++] = startVertexIndex + j + 1;
vertexIndexes[vIndex++] = startVertexIndex + j + 3;
}
}
}
return new Cesium.Geometry({
attributes: new Cesium.GeometryAttributes({
st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 2, values: st,
}), normal: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 3, values: normal,
}),
}), indices: vertexIndexes,
});
}
}