Source: material/points-material.js

import { Material }      from './material.js';
import { ShaderProgram } from '../shader/shader-program.js';

/**
 * Attribute location used by `vec3` position.
 *
 * @type {number}
 */
const POSITION_ATTRIBUTE_LOCATION = 0;

/**
 * Attribute location used by `vec3` color.
 *
 * @type {number}
 */
const COLOR_ATTRIBUTE_LOCATION = 1;

/**
 * Name of the matrix uniform in the shader.
 *
 * @type {string}
 */
const MATRIX_UNIFORM_NAME = 'u_matrix';

/**
 * Name of the color uniform in the shader.
 *
 * @type {string}
 */
const COLOR_UNIFORM_NAME = 'u_color';

/**
 * Name of point size uniform.
 *
 * @type {string}
 */
const POINT_SIZE_UNIFORM_NAME = 'u_pointSize';

/**
 * Name of opacity uniform.
 *
 * @type {string}
 */
const OPACITY_UNIFORM_NAME = 'u_opacity';

/**
 * Name of the use vertex colors usage uniform.
 *
 * @type {string}
 */
const USE_VERTEX_COLOR_UNIFORM_NAME = 'u_useVertexColor';

/**
 * Number of components in a RGB color.
 *
 * @type {number}
 */
const COLOR_COMPONENT_COUNT = 3;

/**
 * Red component index.
 *
 * @type {number}
 */
const COLOR_COMPONENT_RED_INDEX = 0;

/**
 * Green component index.
 *
 * @type {number}
 */
const COLOR_COMPONENT_GREEN_INDEX = 1;

/**
 * Blue component index.
 *
 * @type {number}
 */
const COLOR_COMPONENT_BLUE_INDEX = 2;

/**
 * Default color for points.
 *
 * @type {Float32Array}
 */
const DEFAULT_COLOR = new Float32Array([1.0, 1.0, 1.0]);

/**
 * Default point size in pixels.
 *
 * @type {number}
 */
const DEFAULT_POINT_SIZE = 6.0;

/**
 * Minimum allowed point size.
 *
 * @type {number}
 */
const MIN_POINT_SIZE = 0.0;

/**
 * Default vertex colors usage flag.
 *
 * @type {boolean}
 */
const DEFAULT_USE_VERTEX_COLORS = false;

/**
 * Boolean-as-float value for false.
 *
 * @type {number}
 */
const FLOAT_FALSE = 0.0;

/**
 * Boolean-as-float value for true.
 *
 * @type {number}
 */
const FLOAT_TRUE = 1.0;

/**
 * Point sprite UV center, used for round points.
 *
 * @type {number}
 */
const POINT_COORD_CENTER = 0.5;

/**
 * Point sprite radius, used for round points.
 *
 * @type {number}
 */
const POINT_COORD_RADIUS = 0.5;

/**
 * Position W component, used in vertex shader.
 *
 * @type {number}
 */
const POSITION_W_COMPONENT = 1.0;

/**
 * GLSL vertex shader source code.
 *
 * @type {string}
 */
const VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
layout(location = ${POSITION_ATTRIBUTE_LOCATION}) in vec3 a_position;
layout(location = ${COLOR_ATTRIBUTE_LOCATION}) in vec3 a_color;
uniform mat4 ${MATRIX_UNIFORM_NAME};
uniform vec3 ${COLOR_UNIFORM_NAME};
uniform float ${POINT_SIZE_UNIFORM_NAME};
uniform float ${USE_VERTEX_COLOR_UNIFORM_NAME};
out vec3 v_color;

void main() {
    gl_Position  = ${MATRIX_UNIFORM_NAME} * vec4(a_position, ${POSITION_W_COMPONENT});
    gl_PointSize = ${POINT_SIZE_UNIFORM_NAME};
    v_color      = mix(${COLOR_UNIFORM_NAME}, a_color, ${USE_VERTEX_COLOR_UNIFORM_NAME});
}
`;

/**
 * GLSL fragment shader source code.
 *
 * @type {string}
 */
const FRAGMENT_SHADER_SOURCE = `#version 300 es
precision mediump float;
in vec3 v_color;
uniform float ${OPACITY_UNIFORM_NAME};
out vec4 outColor;

void main() {
    vec2 centered = gl_PointCoord - vec2(${POINT_COORD_CENTER}, ${POINT_COORD_CENTER});
    float dist    = length(centered);

    if (dist > ${POINT_COORD_RADIUS}) {
        discard;
    }

    outColor = vec4(v_color, ${OPACITY_UNIFORM_NAME});
}
`;

/**
 * Options used by `PointsMaterial`.
 *
 * @typedef {Object} PointsMaterialOptions
 * @property {Float32Array | number[]} [color]   - RGB color [red, green, blue] in [0..1] range.
 * @property {number} [pointSize = 6.0]          - Point size in pixels.
 * @property {boolean} [useVertexColors = false] - When true, uses vertex colors.
 */

/**
 * Material for rendering point clouds.
 */
export class PointsMaterial extends Material {

    /**
     * Current RGB color stored as Float32Array([red, green, blue]).
     *
     * @type {Float32Array}
     * @private
     */
    #color = new Float32Array(DEFAULT_COLOR);

    /**
     * Point size in pixels.
     *
     * @type {number}
     * @private
     */
    #pointSize = DEFAULT_POINT_SIZE;

    /**
     * Flag controlling vertex color usage.
     *
     * @type {boolean}
     * @private
     */
    #useVertexColors = DEFAULT_USE_VERTEX_COLORS;

    /**
     * @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context used to compile shaders.
     * @param {PointsMaterialOptions} [options]     - Material options.
     * @throws {TypeError}  When inputs are invalid.
     * @throws {RangeError} When numeric inputs are out of range.
     */
    constructor(webglContext, options = {}) {
        if (options === null || typeof options !== 'object' || Array.isArray(options)) {
            throw new TypeError('`PointsMaterial` expects an options object (plain object).');
        }

        const shaderProgram = new ShaderProgram(webglContext, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE);
        super(webglContext, shaderProgram, { ownsShaderProgram: true });

        const {
            color,
            pointSize       = DEFAULT_POINT_SIZE,
            useVertexColors = DEFAULT_USE_VERTEX_COLORS
        } = options;

        if (color !== undefined) {
            this.setColor(color);
        }

        this.setPointSize(pointSize);
        this.setUseVertexColors(useVertexColors);
    }

    /**
     * Applies per-object uniforms.
     *
     * @param {Float32Array} matrix4 - Transformation matrix passed as `u_matrix`.
     */
    apply(matrix4) {
        this.shaderProgram.setMatrix4(MATRIX_UNIFORM_NAME, matrix4);
        this.shaderProgram.setVector3(COLOR_UNIFORM_NAME, this.#color);
        this.shaderProgram.setFloat(POINT_SIZE_UNIFORM_NAME, this.#pointSize);
        this.shaderProgram.setFloat(USE_VERTEX_COLOR_UNIFORM_NAME, this.#useVertexColors ? FLOAT_TRUE : FLOAT_FALSE);
        this.shaderProgram.setFloat(OPACITY_UNIFORM_NAME, this.opacity);
    }

    /**
     * Sets the RGB color.
     *
     * @param {Float32Array | number[]} color - [red, green, blue] in [0..1] range.
     * @throws {TypeError} When color is invalid.
     */
    setColor(color) {
        if (!Array.isArray(color) && !(color instanceof Float32Array)) {
            throw new TypeError('`PointsMaterial.setColor` expects a number[] or `Float32Array`.');
        }

        if (color.length !== COLOR_COMPONENT_COUNT) {
            throw new TypeError('`PointsMaterial.setColor` expects exactly 3 components [red, green, blue].');
        }

        this.#color[COLOR_COMPONENT_RED_INDEX]   = color[COLOR_COMPONENT_RED_INDEX];
        this.#color[COLOR_COMPONENT_GREEN_INDEX] = color[COLOR_COMPONENT_GREEN_INDEX];
        this.#color[COLOR_COMPONENT_BLUE_INDEX]  = color[COLOR_COMPONENT_BLUE_INDEX];
    }

    /**
     * Sets point size in pixels.
     *
     * @param {number} size - Point size (> 0).
     * @throws {TypeError}  When size is not a finite number.
     * @throws {RangeError} When size is not positive.
     */
    setPointSize(size) {
        if (typeof size !== 'number' || !Number.isFinite(size)) {
            throw new TypeError('`PointsMaterial.setPointSize` expects a finite number.');
        }

        if (size <= MIN_POINT_SIZE) {
            throw new RangeError('`PointsMaterial.setPointSize` expects a positive size.');
        }

        this.#pointSize = size;
    }

    /**
     * Enables or disables vertex colors.
     *
     * @param {boolean} enabled - When true, uses vertex colors.
     * @throws {TypeError} When enabled is not a boolean.
     */
    setUseVertexColors(enabled) {
        if (typeof enabled !== 'boolean') {
            throw new TypeError('`PointsMaterial.setUseVertexColors` expects a boolean.');
        }

        this.#useVertexColors = enabled;
    }

    /**
     * @returns {Float32Array}
     */
    get color() {
        return this.#color;
    }

    /**
     * @returns {number}
     */
    get pointSize() {
        return this.#pointSize;
    }

    /**
     * @returns {boolean}
     */
    get useVertexColors() {
        return this.#useVertexColors;
    }
}