visual/csr/rendered-initials.js

/* global THREE */

const base = require("../../base");
const ripe = base.ripe;

/**
 * The list of supported texture types.
 */
const SUPPORTED_TEXTURE_TYPES = ["base", "displacement", "metallic", "normal", "roughness"];

/**
 * This class encapsulates all logic related to the CSR initials. It provides tools to
 * process and get CSR initials related resources such as textures, materials and 3D
 * objects that can be used to show initials in CSR.
 *
 * @param {Canvas} canvas Canvas uses to process the initials texture.
 * @param {Canvas} canvasDisplacement Canvas uses to process the initials displacement
 * texture.
 * @param {Number} width Width of the canvas. It dictates the resolution on the x axis.
 * @param {Number} height Height of the canvas. It dictates the resolution on the x axis.
 */
ripe.CsrRenderedInitials = function(
    canvas = null,
    canvasDisplacement = null,
    width = null,
    height = null,
    pixelRatio = null,
    options = {}
) {
    if (canvas === null) throw new Error("Canvas is required");
    if (canvasDisplacement === null) throw new Error("CanvasDisplacement is required");
    if (width === null) throw new Error("Width is required");
    if (height === null) throw new Error("Height is required");
    if (pixelRatio === null) throw new Error("PixelRatio is required");

    this.canvas = canvas;
    this.canvasDisplacement = canvasDisplacement;
    this.width = width;
    this.height = height;
    this.pixelRatio = pixelRatio;
    this.textureRenderer = null;
    this.material = null;
    this.points = [];
    this.geometry = null;
    this.mesh = null;
    this.currentText = "";

    this.materialTexturesRefs = {
        map: null,
        displacementMap: null,
        normalMap: null
    };

    this.rawTexturesRefs = {
        base: null,
        displacement: null,
        metallic: null,
        normal: null,
        roughness: null
    };

    this.cookedTexturesRefs = {
        base: null,
        displacement: null,
        metallic: null,
        normal: null,
        roughness: null
    };

    const DEFAULT_TEXTURE_SETTINGS = {
        wrapS: THREE.RepeatWrapping,
        wrapT: THREE.RepeatWrapping,
        offset: new THREE.Vector2(0, 0),
        repeat: new THREE.Vector2(1, 1),
        rotation: 0,
        center: new THREE.Vector2(0, 0),
        encoding: THREE.sRGBEncoding
    };

    // unpacks the CSR Initials Renderer options
    const curveOpts = options.curveOptions || {};
    this.curveOptions = {
        type: curveOpts.type !== undefined ? curveOpts.type : "centripetal",
        tension: curveOpts.tension !== undefined ? curveOpts.tension : 0.5
    };
    const textOpts = options.textOptions || {};
    this.textOptions = {
        font: textOpts.font !== undefined ? textOpts.font : "Arial",
        fontSize: textOpts.fontSize !== undefined ? textOpts.fontSize : 280,
        xOffset: textOpts.xOffset !== undefined ? textOpts.xOffset : 0,
        yOffset: textOpts.yOffset !== undefined ? textOpts.yOffset : 0,
        lineWidth: textOpts.lineWidth !== undefined ? textOpts.lineWidth : 5,
        displacementMapTextBlur:
            textOpts.displacementMapTextBlur !== undefined ? textOpts.displacementMapTextBlur : 1.5,
        normalMapBlurIntensity:
            textOpts.normalMapBlurIntensity !== undefined ? textOpts.normalMapBlurIntensity : 1
    };
    const materialOpts = options.materialOptions || {};
    this.materialOptions = {
        color:
            materialOpts.color !== undefined
                ? new THREE.Color(materialOpts.color)
                : new THREE.Color("#ffffff"),
        displacementScale:
            materialOpts.displacementScale !== undefined ? materialOpts.displacementScale : 25,
        displacementBias:
            materialOpts.displacementBias !== undefined ? materialOpts.displacementBias : 0,
        emissive:
            materialOpts.emissive !== undefined
                ? new THREE.Color(materialOpts.emissive)
                : new THREE.Color("#000000"),
        emissiveIntensity:
            materialOpts.emissiveIntensity !== undefined ? materialOpts.emissiveIntensity : 1,
        flatShading: materialOpts.flatShading !== undefined ? materialOpts.flatShading : false,
        metalness: materialOpts.metalness !== undefined ? materialOpts.metalness : 0,
        roughness: materialOpts.roughness !== undefined ? materialOpts.roughness : 1,
        wireframe: materialOpts.wireframe !== undefined ? materialOpts.wireframe : false
    };
    const meshOpts = options.meshOptions || {};
    this.meshOptions = {
        widthSegments: meshOpts.widthSegments !== undefined ? meshOpts.widthSegments : 1000,
        heightSegments: meshOpts.heightSegments !== undefined ? meshOpts.heightSegments : 100
    };
    SUPPORTED_TEXTURE_TYPES.forEach(type => {
        const key = this._textureOptionsKey(type);
        const textureTypeOptions = options[key] || {};
        this[key] = { ...DEFAULT_TEXTURE_SETTINGS, ...textureTypeOptions };
    });

    // sets the CSR Initials Renderer size
    this.setSize(width, height);

    // inits the CSR Initials Renderer material
    this.material = new THREE.MeshStandardMaterial({
        transparent: true,
        ...this.materialOptions
    });
};
ripe.CsrRenderedInitials.prototype.constructor = ripe.CsrRenderedInitials;

/**
 * Sets the initials text.
 *
 * @param {String} text Initials text.
 */
ripe.CsrRenderedInitials.prototype.setInitials = function(text) {
    this.currentText = text;

    // cleans up textures that are going to be replaced
    this._destroyMaterialTextures();

    // generates the necessary text textures
    const textTexture = this._textToTexture(text);
    const displacementTexture = this._textToDisplacementTexture(text);

    // generates a normal map for the text displacement map texture
    this.materialTexturesRefs.normalMap = ripe.CsrUtils.normalMapFromCanvas(
        this.canvasDisplacement
    );

    // blurs normal map texture to avoid normal map color banding
    const blurIntensity = this.textOptions.normalMapBlurIntensity;
    if (blurIntensity > 0) {
        const tempRef = this._blurTexture(this.materialTexturesRefs.normalMap, blurIntensity);
        this.materialTexturesRefs.normalMap.dispose();
        this.materialTexturesRefs.normalMap = tempRef;
    }

    // applies the patterns to the text textures
    this.materialTexturesRefs.map = this._mixPatternWithTexture(
        textTexture,
        this.cookedTexturesRefs.base
    );
    this.materialTexturesRefs.displacementMap = this._mixPatternWithDisplacementTexture(
        displacementTexture,
        this.cookedTexturesRefs.displacement
    );

    // cleans up temporary textures
    textTexture.dispose();
    displacementTexture.dispose();

    // updates the initials material
    this.material.map = this.materialTexturesRefs.map;
    this.material.displacementMap = this.materialTexturesRefs.displacementMap;
    this.material.normalMap = this.materialTexturesRefs.normalMap;

    // marks material to do a internal update
    this.material.needsUpdate = true;
};

/**
 * Sets the reference points that are used when generating the curve that bends the initials mesh.
 *
 * @param {Array} points Array with THREE.Vector3 reference points for the curve used to bend
 * the mesh.
 */
ripe.CsrRenderedInitials.prototype.setPoints = function(points) {
    this.points = points;

    // updates the existing mesh geometry if the mesh already exists
    if (this.mesh) this._morphPlaneGeometry(this.geometry, this.points);
};

/**
 * Gets the initials material. This material can be applied to a mesh in order to obtain
 * the 3D text effect.
 *
 * @returns {THREE.Material} Material that makes the 3D text effect.
 */
ripe.CsrRenderedInitials.prototype.getMaterial = function() {
    if (!this.material) throw new Error("The material doesn't exist");
    return this.material;
};

/**
 * Gets the initials 3D object.
 *
 * @returns {THREE.Object3D} Mesh that will have the initials text.
 */
ripe.CsrRenderedInitials.prototype.getMesh = function() {
    // ensures mesh exists
    if (!this.mesh) this._buildInitialsMesh();

    return this.mesh;
};

/**
 * Sets the texture specified by it's type. The supported types are the following:
 * - base: This texture is the diffuse pattern that is applied to the initials characters.
 * - displacement: This texture is the height map pattern that is applied to the initials
 * characters.
 * - metallic: This texture is the metallic texture that is applied to the initials characters.
 * - normal: This texture is the normal map pattern that is applied to the initials characters.
 * - roughness: This texture is the roughness texture that is applied to the initials characters.
 *
 * @param {String} type The texture type name.
 * @param {THREE.Texture} texture The texture to set for the specified type.
 * @param {Object} options Options to apply to the texture.
 */
ripe.CsrRenderedInitials.prototype.setTexture = function(type, texture, options = {}) {
    this._verifyTextureType(type);

    this[this._textureOptionsKey(type)] = { ...this[this._textureOptionsKey(type)], ...options };

    // cleanups resources
    if (this.rawTexturesRefs[type]) this.rawTexturesRefs[type].dispose();
    if (this.cookedTexturesRefs[type]) this.cookedTexturesRefs[type].dispose();

    // saves raw texture clone so it can be reused
    this.rawTexturesRefs[type] = texture.clone();
    this.rawTexturesRefs[type].needsUpdate = true;

    // applies texture options by precooking the texture
    this.cookedTexturesRefs[type] = this._preCookTexture(
        this.rawTexturesRefs[type],
        this[this._textureOptionsKey(type)]
    );
};

/**
 * Sets the texture attributes.
 *
 * @param {Object} options Options to apply to the texture.
 * @param {String} type The texture type name.
 */
ripe.CsrRenderedInitials.prototype.setTextureOptions = function(type, options = {}) {
    this._verifyTextureType(type);
    if (!this.rawTexturesRefs[type]) {
        throw new Error(`Can't apply ${type} texture options, the texture is not set`);
    }

    // update texture options
    this[this._textureOptionsKey(type)] = { ...this[this._textureOptionsKey(type)], ...options };

    // cleanups resources then applies the texture options by precooking the texture
    if (this.cookedTexturesRefs[type]) this.cookedTexturesRefs[type].dispose();
    this.cookedTexturesRefs[type] = this._preCookTexture(
        this.rawTexturesRefs[type],
        this[this._textureOptionsKey(type)]
    );
};

/**
 * Sets the initials renderer width and height. It also updates the texture renderer used
 * by this instance.
 *
 * @param {Number} width Number for the width in pixels.
 * @param {Number} height Number for the height in pixels.
 */
ripe.CsrRenderedInitials.prototype.setSize = function(width = null, height = null) {
    if (width === null) throw new Error("Width is required");
    if (height === null) throw new Error("Height is required");

    this.width = width;
    this.height = height;
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    this.canvasDisplacement.width = this.width;
    this.canvasDisplacement.height = this.height;

    // rebuilds texture renderer with the new size
    if (this.textureRenderer) this.textureRenderer.destroy();
    this.textureRenderer = new ripe.CsrTextureRenderer(width, height, this.pixelRatio);
};

/**
 * Updates initials renderer state by updating it's options.
 *
 * @param {Object} options Set of optional parameters to adjust the initials renderer.
 */
ripe.CsrRenderedInitials.prototype.updateOptions = function(options = {}) {
    let updateInitials = false;
    let updateMaterial = false;
    let updateMesh = false;
    const updateTextures = [];

    if (options.textOptions) {
        this.textOptions = { ...this.textOptions, ...options.textOptions };
        updateInitials = true;
    }
    if (options.materialOptions) {
        this.materialOptions = { ...this.materialOptions, ...options.materialOptions };
        updateMaterial = true;
        updateInitials = true;
    }
    if (options.meshOptions) {
        this.meshOptions = { ...this.meshOptions, ...options.meshOptions };
        updateMesh = true;
    }
    SUPPORTED_TEXTURE_TYPES.forEach(type => {
        const key = this._textureOptionsKey(type);
        if (options[key]) {
            this[key] = { ...this[key], ...options[key] };
            updateTextures.push(key);
            updateInitials = true;
        }
    });

    // performs update operations. The order is important
    updateTextures.forEach(type => {
        this.setTextureOptions(type, this[this._textureOptionsKey(type)]);
    });
    if (updateMaterial) {
        ripe.CsrUtils.applyOptions(this.materialOptions);
        this.material.needsUpdate = true;
    }
    if (updateInitials) this.rerenderInitials();
    if (updateMesh) this._buildInitialsMesh();
};

ripe.CsrRenderedInitials.prototype.rerenderInitials = function() {
    this.setInitials(this.currentText);
};

/**
 * Cleanups the `CsrRenderedInitials` instance thus avoiding memory leak issues.
 */
ripe.CsrRenderedInitials.prototype.destroy = function() {
    // cleans up the texture renderer
    this.textureRenderer.destroy();

    // cleans up textures
    this._destroyRawTextures();
    this._destroyCookedTextures();
    this._destroyMaterialTextures();

    // cleans up the material
    if (this.material) this.material.dispose();

    // cleans up the initials mesh
    this._destroyMesh();
};

/**
 * Verifies if the type of texture is supported.
 *
 * @param {String} type The type of texture.
 */
ripe.CsrRenderedInitials.prototype._verifyTextureType = function(type) {
    const isValid = SUPPORTED_TEXTURE_TYPES.includes(type);
    if (!isValid) throw new Error(`The texture type "${type}" is not supported`);
};

/**
 * Builds the texture options key for the provided texture type.
 */
ripe.CsrRenderedInitials.prototype._textureOptionsKey = function(type) {
    this._verifyTextureType(type);
    return `${type}TextureOptions`;
};

/**
 * Cleanups everything used only by the initials mesh.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._destroyMesh = function() {
    if (!this.mesh) return;

    if (this.geometry) this.geometry.dispose();
    if (this.mesh.geometry) this.mesh.geometry.dispose();
    this.mesh = null;
};

/**
 * Cleanups raw textured.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._destroyRawTextures = function() {
    if (this.rawTexturesRefs.base) this.rawTexturesRefs.base.dispose();
    if (this.rawTexturesRefs.displacement) this.rawTexturesRefs.displacement.dispose();
    this.rawTexturesRefs = {
        base: null,
        displacement: null
    };
};

/**
 * Cleanups cooked textures.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._destroyCookedTextures = function() {
    if (this.cookedTexturesRefs.base) this.cookedTexturesRefs.base.dispose();
    if (this.cookedTexturesRefs.displacement) this.cookedTexturesRefs.displacement.dispose();
    this.cookedTexturesRefs = {
        base: null,
        displacement: null
    };
};

/**
 * Cleanups textures mapped to the material.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._destroyMaterialTextures = function() {
    if (this.materialTexturesRefs.map) this.materialTexturesRefs.map.dispose();
    if (this.materialTexturesRefs.displacementMap) {
        this.materialTexturesRefs.displacementMap.dispose();
    }
    if (this.materialTexturesRefs.normalMap) this.materialTexturesRefs.normalMap.dispose();
    this.materialTexturesRefs = {
        map: null,
        displacementMap: null,
        normalMap: null
    };
};

/**
 * Builds the initials mesh 3D object. If a mesh already exists, it will rebuild it.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._buildInitialsMesh = function() {
    // cleans current mesh
    if (this.mesh) this._destroyMesh();

    // generates the initials plane geometry
    this.geometry = this._buildGeometry();

    // creates the initials mesh
    this.mesh = new THREE.Mesh(this.geometry, this.material);
};

/**
 * Builds the mesh geometry. If points were set, it will bend the geometry accordingly.
 *
 * @returns {THREE.BufferGeometry} Returns a BufferGeometry instance.
 */
ripe.CsrRenderedInitials.prototype._buildGeometry = function() {
    const geometry = new THREE.PlaneBufferGeometry(
        this.width,
        this.height,
        this.meshOptions.widthSegments,
        this.meshOptions.heightSegments
    );

    // no points to generate a curve so returns the flat geometry
    if (this.points.length < 2) return geometry;

    // morphs the plane geometry using the points as reference
    this._morphPlaneGeometry(geometry, this.points);

    return geometry;
};

/**
 * Morhps a plane geometry by following a curve as reference.
 *
 * @param {BufferGeometry} geometry The plane geometry to be morphed.
 * @param {Array} points Array with THREE.Vector3 reference points for the morphing curve.
 * @returns {THREE.BufferGeometry} The morphed geometry.
 */
ripe.CsrRenderedInitials.prototype._morphPlaneGeometry = function(geometry, points) {
    // creates a curve based on the reference points
    const curve = new THREE.CatmullRomCurve3(
        points,
        false,
        this.curveOptions.type,
        this.curveOptions.tension
    );

    // calculates the curve width
    const curveWidth = Math.round(curve.getLength());

    // get the curve discrete points
    const curvePoints = curve.getSpacedPoints(curveWidth);

    // calculate offsets needed to iterate the geometry vertexes
    const segments = curveWidth >= this.width ? this.width : curveWidth;
    const curvePointStep = segments / this.meshOptions.widthSegments;
    const curvePointOffset =
        curveWidth > this.width ? Math.floor(curveWidth / 2 - this.width / 2) : 0;

    // iterates the geometry vertexes and updates their position to follow the curve
    const geoPos = geometry.attributes.position;
    for (let i = 0; i <= this.meshOptions.heightSegments; i++) {
        for (
            let j = 0, curvePointIdx = curvePointOffset;
            j <= this.meshOptions.widthSegments;
            j++, curvePointIdx += curvePointStep
        ) {
            const vertexIdx = j + i + this.meshOptions.widthSegments * i;
            const curvePoint = curvePoints[Math.round(curvePointIdx)];
            geoPos.setXYZ(
                vertexIdx,
                curvePoint.x,
                geoPos.getY(vertexIdx) + curvePoint.y,
                curvePoint.z
            );
        }
    }

    // recalculates normals and tangents
    geometry.computeVertexNormals();
    geometry.computeTangents();

    // marks geometry to do a internal update
    geometry.attributes.position.needsUpdate = true;

    return geometry;
};

/**
 * Transforms a string into a texture.
 *
 * @param {String} text Text to be transformed into a texture.
 * @returns {THREE.Texture} Texture with the initials text.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._textToTexture = function(text) {
    const width = this.width;
    const height = this.height;
    const font = this.textOptions.font;
    const fontSize = this.textOptions.fontSize;
    const xOffset = this.textOptions.xOffset;
    const yOffset = this.textOptions.yOffset;
    const lineWidth = this.textOptions.lineWidth;

    const ctx = this.canvas.getContext("2d");

    // cleans canvas
    ctx.clearRect(0, 0, width, height);

    ctx.font = `${fontSize}px ${font}`;
    ctx.fillStyle = "#ffffff";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    // adds a little thickness so that when the displacement is applied,
    // the color expands to the edges of the text
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = "#ffffff";

    // writes text to the center of the canvas
    const posX = width / 2;
    const posY = height / 2;
    ctx.fillText(text, posX + xOffset, posY + yOffset);
    ctx.strokeText(text, posX + xOffset, posY + yOffset);

    // creates texture from canvas
    const texture = new THREE.CanvasTexture(this.canvas);

    return texture;
};

/**
 * Transforms a string into a displacement texture.
 *
 * @param {String} text Text to be transformed into a texture.
 * @returns {THREE.Texture} Texture with the initials text.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._textToDisplacementTexture = function(text) {
    const width = this.width;
    const height = this.height;
    const font = this.textOptions.font;
    const fontSize = this.textOptions.fontSize;
    const xOffset = this.textOptions.xOffset;
    const yOffset = this.textOptions.yOffset;
    const blur = this.textOptions.displacementMapTextBlur;

    const ctx = this.canvasDisplacement.getContext("2d");

    // cleans canvas with black color
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);

    ctx.font = `${fontSize}px ${font}`;
    ctx.fillStyle = "#ffffff";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    // adds blur filter to attenuate the displacement
    // more blur equals less displacement which means more rounded edges
    ctx.filter = `blur(${blur}px)`;

    // writes text to the center of the canvas
    const posX = width / 2;
    const posY = height / 2;
    ctx.fillText(text, posX + xOffset, posY + yOffset);

    // creates texture from canvas
    const texture = new THREE.CanvasTexture(this.canvasDisplacement);

    return texture;
};

/**
 * This method is used to go around the Three.js limitation of not respecting all
 * THREE.Texture properties when mapping some textures such as displacement maps,
 * normal maps, etc.
 *
 * @param {THREE.Texture} texture Texture that is going to be pre cook.
 * @param {Object} options Options to apply to the texture.
 * @returns {THREE.Texture} Texture with the result of the applied options.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._preCookTexture = function(texture, options) {
    texture = ripe.CsrUtils.applyOptions(texture, options);

    // generates a texture with the updated options
    const material = new THREE.MeshBasicMaterial({ transparent: true, map: texture });
    const updatedTexture = this.textureRenderer.textureFromMaterial(material);

    // cleans up the temporary material
    material.dispose();

    return updatedTexture;
};

/**
 * Blurs a texture by doing a Gaussian blur pass.
 *
 * @param {THREE.Texture} texture Texture to blur.
 * @param {Number} blurIntensity Intensity of blur filter that is going to be applied.
 * @returns {THREE.Texture} The blurred texture.
 */
ripe.CsrRenderedInitials.prototype._blurTexture = function(texture, blurIntensity = 1) {
    // creates a material to run a shader that blurs the texture
    const material = new THREE.ShaderMaterial({
        uniforms: THREE.UniformsUtils.merge([
            {
                baseTexture: {
                    type: "t",
                    value: texture
                },
                h: {
                    type: "f",
                    value: 1 / (this.width * blurIntensity)
                },
                v: {
                    type: "f",
                    value: 1 / (this.height * blurIntensity)
                }
            }
        ]),
        vertexShader: ripe.CsrUtils.BlurShader.vertexShader,
        fragmentShader: ripe.CsrUtils.BlurShader.fragmentShader
    });

    // generates a blurred texture
    const blurredTexture = this.textureRenderer.textureFromMaterial(material);

    // cleans up the temporary material
    material.dispose();

    return blurredTexture;
};

/**
 * Mixes a pattern with a texture. It's used to apply a pattern to the initials text.
 *
 * @param {THREE.Texture} texture Texture with the initials text.
 * @param {THREE.Texture} patternTexture Texture with the pattern to apply.
 * @returns {THREE.Texture} Texture with the pattern applied to the initials text.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._mixPatternWithTexture = function(texture, patternTexture) {
    // returns the original texture if no pattern texture is provided
    if (!patternTexture) return texture;

    // creates a material to run a shader that applies a pattern to a texture
    const material = new THREE.ShaderMaterial({
        uniforms: THREE.UniformsUtils.merge([
            {
                baseTexture: {
                    type: "t",
                    value: texture
                },
                patternTexture: {
                    type: "t",
                    value: patternTexture
                }
            }
        ]),
        vertexShader: ripe.CsrUtils.PatternMixerShader.vertexShader,
        fragmentShader: ripe.CsrUtils.PatternMixerShader.fragmentShader
    });

    // generates a texture with the textures mixed
    const mixedTexture = this.textureRenderer.textureFromMaterial(material);

    // cleans up the temporary material
    material.dispose();

    return mixedTexture;
};

/**
 * Mixes a pattern with a displacement texture. It's used to apply a pattern to the
 * height map texture of the initials text.
 *
 * @param {THREE.Texture} texture Texture with the initials height map texture.
 * @param {THREE.Texture} patternTexture Texture with the pattern to apply.
 * @param {Number} patternIntensity The intensity of on which the pattern will be applied. It
 * ranges from 0 to 1 being that the higher the number the more intensely the pattern will be
 * applied to the height map texture.
 * @returns {THREE.Texture} Texture with the pattern applied to the initials text.
 *
 * @private
 */
ripe.CsrRenderedInitials.prototype._mixPatternWithDisplacementTexture = function(
    texture,
    patternTexture,
    patternIntensity = 1
) {
    // returns the original texture if no pattern texture is provided
    if (!patternTexture) return texture;

    // creates a material to run a shader that applies a pattern to a height map texture
    const material = new THREE.ShaderMaterial({
        uniforms: THREE.UniformsUtils.merge([
            {
                baseTexture: {
                    type: "t",
                    value: texture
                },
                patternTexture: {
                    type: "t",
                    value: patternTexture
                },
                patternIntensity: {
                    type: "f",
                    value: patternIntensity
                }
            }
        ]),
        vertexShader: ripe.CsrUtils.HeightmapPatternMixerShader.vertexShader,
        fragmentShader: ripe.CsrUtils.HeightmapPatternMixerShader.fragmentShader
    });

    // generates a texture with the textures mixed
    const mixedTexture = this.textureRenderer.textureFromMaterial(material);

    // cleans up the temporary material
    material.dispose();

    return mixedTexture;
};