util/texture.js

import Texture2D from '../Texture2D';
import TextureCube from '../TextureCube';
import vendor from '../core/vendor';
import EnvironmentMapPass from '../prePass/EnvironmentMap';
import Skydome from '../plugin/Skydome';
import Scene from '../Scene';

import dds from './dds';
import hdr from './hdr';

/**
 * @alias clay.util.texture
 */
var textureUtil = {
    /**
     * @param  {string|object} path
     * @param  {object} [option]
     * @param  {Function} [onsuccess]
     * @param  {Function} [onerror]
     * @return {clay.Texture}
     */
    loadTexture: function (path, option, onsuccess, onerror) {
        var texture;
        if (typeof(option) === 'function') {
            onsuccess = option;
            onerror = onsuccess;
            option = {};
        }
        else {
            option = option || {};
        }
        if (typeof(path) === 'string') {
            if (path.match(/.hdr$/) || option.fileType === 'hdr') {
                texture = new Texture2D({
                    width: 0,
                    height: 0,
                    sRGB: false
                });
                textureUtil._fetchTexture(
                    path,
                    function (data) {
                        hdr.parseRGBE(data, texture, option.exposure);
                        texture.dirty();
                        onsuccess && onsuccess(texture);
                    },
                    onerror
                );
                return texture;
            }
            else if (path.match(/.dds$/) || option.fileType === 'dds') {
                texture = new Texture2D({
                    width: 0,
                    height: 0
                });
                textureUtil._fetchTexture(
                    path,
                    function (data) {
                        dds.parse(data, texture);
                        texture.dirty();
                        onsuccess && onsuccess(texture);
                    },
                    onerror
                );
            }
            else {
                texture = new Texture2D();
                texture.load(path);
                texture.success(onsuccess);
                texture.error(onerror);
            }
        }
        else if (typeof path === 'object' && typeof(path.px) !== 'undefined') {
            texture = new TextureCube();
            texture.load(path);
            texture.success(onsuccess);
            texture.error(onerror);
        }
        return texture;
    },

    /**
     * Load a panorama texture and render it to a cube map
     * @param  {clay.Renderer} renderer
     * @param  {string} path
     * @param  {clay.TextureCube} cubeMap
     * @param  {object} [option]
     * @param  {boolean} [option.encodeRGBM]
     * @param  {number} [option.exposure]
     * @param  {Function} [onsuccess]
     * @param  {Function} [onerror]
     */
    loadPanorama: function (renderer, path, cubeMap, option, onsuccess, onerror) {
        var self = this;

        if (typeof(option) === 'function') {
            onsuccess = option;
            onerror = onsuccess;
            option = {};
        }
        else {
            option = option || {};
        }

        textureUtil.loadTexture(path, option, function (texture) {
            // PENDING
            texture.flipY = option.flipY || false;
            self.panoramaToCubeMap(renderer, texture, cubeMap, option);
            texture.dispose(renderer);
            onsuccess && onsuccess(cubeMap);
        }, onerror);
    },

    /**
     * Render a panorama texture to a cube map
     * @param  {clay.Renderer} renderer
     * @param  {clay.Texture2D} panoramaMap
     * @param  {clay.TextureCube} cubeMap
     * @param  {Object} option
     * @param  {boolean} [option.encodeRGBM]
     */
    panoramaToCubeMap: function (renderer, panoramaMap, cubeMap, option) {
        var environmentMapPass = new EnvironmentMapPass();
        var skydome = new Skydome({
            scene: new Scene()
        });
        skydome.setEnvironmentMap(panoramaMap);

        option = option || {};
        if (option.encodeRGBM) {
            skydome.material.define('fragment', 'RGBM_ENCODE');
        }

        // Share sRGB
        cubeMap.sRGB = panoramaMap.sRGB;

        environmentMapPass.texture = cubeMap;
        environmentMapPass.render(renderer, skydome.scene);
        environmentMapPass.texture = null;
        environmentMapPass.dispose(renderer);
        return cubeMap;
    },

    /**
     * Convert height map to normal map
     * @param {HTMLImageElement|HTMLCanvasElement} image
     * @param {boolean} [checkBump=false]
     * @return {HTMLCanvasElement}
     */
    heightToNormal: function (image, checkBump) {
        var canvas = document.createElement('canvas');
        var width = canvas.width = image.width;
        var height = canvas.height = image.height;
        var ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0, width, height);
        checkBump = checkBump || false;
        var srcData = ctx.getImageData(0, 0, width, height);
        var dstData = ctx.createImageData(width, height);
        for (var i = 0; i < srcData.data.length; i += 4) {
            if (checkBump) {
                var r = srcData.data[i];
                var g = srcData.data[i + 1];
                var b = srcData.data[i + 2];
                var diff = Math.abs(r - g) + Math.abs(g - b);
                if (diff > 20) {
                    console.warn('Given image is not a height map');
                    return image;
                }
            }
            // Modified from http://mrdoob.com/lab/javascript/height2normal/
            var x1, y1, x2, y2;
            if (i % (width * 4) === 0) {
                // left edge
                x1 = srcData.data[i];
                x2 = srcData.data[i + 4];
            }
            else if (i % (width * 4) === (width - 1) * 4) {
                // right edge
                x1 = srcData.data[i - 4];
                x2 = srcData.data[i];
            }
            else {
                x1 = srcData.data[i - 4];
                x2 = srcData.data[i + 4];
            }

            if (i < width * 4) {
                // top edge
                y1 = srcData.data[i];
                y2 = srcData.data[i + width * 4];
            }
            else if (i > width * (height - 1) * 4) {
                // bottom edge
                y1 = srcData.data[i - width * 4];
                y2 = srcData.data[i];
            }
            else {
                y1 = srcData.data[i - width * 4];
                y2 = srcData.data[i + width * 4];
            }

            dstData.data[i] = (x1 - x2) + 127;
            dstData.data[i + 1] = (y1 - y2) + 127;
            dstData.data[i + 2] = 255;
            dstData.data[i + 3] = 255;
        }
        ctx.putImageData(dstData, 0, 0);
        return canvas;
    },

    /**
     * Convert height map to normal map
     * @param {HTMLImageElement|HTMLCanvasElement} image
     * @param {boolean} [checkBump=false]
     * @param {number} [threshold=20]
     * @return {HTMLCanvasElement}
     */
    isHeightImage: function (img, downScaleSize, threshold) {
        if (!img || !img.width || !img.height) {
            return false;
        }

        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var size = downScaleSize || 32;
        threshold = threshold || 20;
        canvas.width = canvas.height = size;
        ctx.drawImage(img, 0, 0, size, size);
        var srcData = ctx.getImageData(0, 0, size, size);
        for (var i = 0; i < srcData.data.length; i += 4) {
            var r = srcData.data[i];
            var g = srcData.data[i + 1];
            var b = srcData.data[i + 2];
            var diff = Math.abs(r - g) + Math.abs(g - b);
            if (diff > threshold) {
                return false;
            }
        }
        return true;
    },

    _fetchTexture: function (path, onsuccess, onerror) {
        vendor.request.get({
            url: path,
            responseType: 'arraybuffer',
            onload: onsuccess,
            onerror: onerror
        });
    },

    /**
     * Create a chessboard texture
     * @param  {number} [size]
     * @param  {number} [unitSize]
     * @param  {string} [color1]
     * @param  {string} [color2]
     * @return {clay.Texture2D}
     */
    createChessboard: function (size, unitSize, color1, color2) {
        size = size || 512;
        unitSize = unitSize || 64;
        color1 = color1 || 'black';
        color2 = color2 || 'white';

        var repeat = Math.ceil(size / unitSize);

        var canvas = document.createElement('canvas');
        canvas.width = size;
        canvas.height = size;
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = color2;
        ctx.fillRect(0, 0, size, size);

        ctx.fillStyle = color1;
        for (var i = 0; i < repeat; i++) {
            for (var j = 0; j < repeat; j++) {
                var isFill = j % 2 ? (i % 2) : (i % 2 - 1);
                if (isFill) {
                    ctx.fillRect(i * unitSize, j * unitSize, unitSize, unitSize);
                }
            }
        }

        var texture = new Texture2D({
            image: canvas,
            anisotropic: 8
        });

        return texture;
    },

    /**
     * Create a blank pure color 1x1 texture
     * @param  {string} color
     * @return {clay.Texture2D}
     */
    createBlank: function (color) {
        var canvas = document.createElement('canvas');
        canvas.width = 1;
        canvas.height = 1;
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = color;
        ctx.fillRect(0, 0, 1, 1);

        var texture = new Texture2D({
            image: canvas
        });

        return texture;
    }
};

export default textureUtil;