Source: modules/wind/MagoWind.js

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,
        });
    }

}