UNPKG

@egjs/view360

Version:

360 integrated viewing solution from inside-out view to outside-in view. It provides user-friendly service by rotating 360 degrees through various user interaction such as motion sensor and touch.

286 lines (238 loc) 8.49 kB
import agent from "@egjs/agent"; import WebGLUtils from "../WebGLUtils"; import { util as mathUtil } from "../../utils/math-util"; import { CubemapConfig } from "../../types/internal"; import Renderer from "./Renderer"; class CubeRenderer extends Renderer { public static extractOrder(imageConfig: CubemapConfig) { return imageConfig.order || "RLUDBF"; } private static _VERTEX_POSITION_DATA: number[] | null = null; private static _INDEX_DATA: number[] | null = null; public getVertexPositionData() { CubeRenderer._VERTEX_POSITION_DATA = CubeRenderer._VERTEX_POSITION_DATA !== null ? CubeRenderer._VERTEX_POSITION_DATA : [ // back 1, -1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, // front -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, // top -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1, // bottom 1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, 1, // right 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, -1, // left -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, 1 ]; return CubeRenderer._VERTEX_POSITION_DATA; } public getIndexData() { if (CubeRenderer._INDEX_DATA) { return CubeRenderer._INDEX_DATA; } const indexData: number[] = []; const vertexPositionData = this.getVertexPositionData(); for (let i = 0; i < (vertexPositionData.length / 3); i += 4) { indexData.push( i, i + 2, i + 1, i, i + 3, i + 2 ); } CubeRenderer._INDEX_DATA = indexData; return indexData; } public getTextureCoordData({ image, imageConfig }: { image: HTMLImageElement | HTMLVideoElement; imageConfig: CubemapConfig; }) { const vertexOrder = "BFUDRL"; const order = CubeRenderer.extractOrder(imageConfig); const base = this.getVertexPositionData(); const tileConfig = this._extractTileConfig(imageConfig); const elemSize = 3; const vertexPerTile = 4; const { trim } = imageConfig; const texCoords = vertexOrder.split("") .map(face => tileConfig[order.indexOf(face)]) .map((config, i) => { const rotation = Math.floor(config.rotation / 90); const ordermap = config.flipHorizontal ? [0, 1, 2, 3] : [1, 0, 3, 2]; for (let r = 0; r < Math.abs(rotation); r++) { if ((config.flipHorizontal && rotation > 0) || (!config.flipHorizontal && rotation < 0)) { ordermap.push(ordermap.shift()!); } else { ordermap.unshift(ordermap.pop()!); } } const elemPerTile = elemSize * vertexPerTile; const tileVertex = base.slice(i * elemPerTile, i * elemPerTile + elemPerTile); const tileTemp: number[][] = []; for (let j = 0; j < vertexPerTile; j++) { tileTemp[ordermap[j]] = tileVertex.splice(0, elemSize); } return tileTemp; }) .map(coord => this._shrinkCoord({ image, faceCoords: coord, trim })) .reduce((acc: number[], val: number[][]) => [ ...acc, ...val.reduce((coords, coord) => [...coords, ...coord], []) ], []); return texCoords; } public getVertexShaderSource() { return ` attribute vec3 aVertexPosition; attribute vec3 aTextureCoord; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; varying highp vec3 vVertexDirectionVector; void main(void) { vVertexDirectionVector = aTextureCoord; gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); }`; } public getFragmentShaderSource() { return ` precision highp float; uniform samplerCube uSampler; varying highp vec3 vVertexDirectionVector; void main(void) { gl_FragColor = textureCube(uSampler, vVertexDirectionVector); }`; } public updateTexture(gl: WebGLRenderingContext, image: HTMLImageElement | HTMLVideoElement, imageConfig: CubemapConfig) { const baseOrder = "RLUDBF"; const order = CubeRenderer.extractOrder(imageConfig); const orderMap = {}; order.split("").forEach((v, i) => { orderMap[v] = i; }); try { if (image instanceof Array) { for (let surfaceIdx = 0; surfaceIdx < 6; surfaceIdx++) { const tileIdx = orderMap[baseOrder[surfaceIdx]]; WebGLUtils.texImage2D(gl, gl.TEXTURE_CUBE_MAP_POSITIVE_X + surfaceIdx, image[tileIdx]); } } else { const maxCubeMapTextureSize = this.getMaxCubeMapTextureSize(gl, image); for (let surfaceIdx = 0; surfaceIdx < 6; surfaceIdx++) { const tileIdx = orderMap[baseOrder[surfaceIdx]]; const tile = this.extractTileFromImage( image, tileIdx, maxCubeMapTextureSize ); WebGLUtils.texImage2D(gl, gl.TEXTURE_CUBE_MAP_POSITIVE_X + surfaceIdx, tile); } } } catch (e) { this._triggerError(e); } } public bindTexture(gl: WebGLRenderingContext, texture: WebGLTexture, image: HTMLImageElement | HTMLVideoElement, imageConfig: CubemapConfig) { gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); this.updateTexture(gl, image, imageConfig); } public getSourceTileSize(image: HTMLImageElement | HTMLVideoElement) { const {width, height} = this.getDimension(image); const aspectRatio = width / height; let inputTextureSize; if (aspectRatio === 1 / 6) { inputTextureSize = width; } else if (aspectRatio === 6) { inputTextureSize = height; } else if (aspectRatio === 2 / 3) { inputTextureSize = width / 2; } else { inputTextureSize = width / 3; } return inputTextureSize; } public extractTileFromImage(image: HTMLImageElement | HTMLVideoElement, tileIdx: number, outputTextureSize: number) { const {width} = this.getDimension(image); const inputTextureSize = this.getSourceTileSize(image); const canvas = document.createElement("canvas"); canvas.width = outputTextureSize; canvas.height = outputTextureSize; const context = canvas.getContext("2d"); const tilePerRow = width / inputTextureSize; const x = inputTextureSize * tileIdx % (inputTextureSize * tilePerRow); const y = Math.floor(tileIdx / tilePerRow) * (inputTextureSize); context!.drawImage( image, x, y, inputTextureSize, inputTextureSize, 0, 0, outputTextureSize, outputTextureSize ); return canvas; } public getMaxCubeMapTextureSize(gl: WebGLRenderingContext, image: HTMLImageElement | HTMLVideoElement) { const agentInfo = agent(); const maxCubeMapTextureSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); let imageWidth = this.getSourceTileSize(image); if (agentInfo.browser.name === "ie" && agentInfo.browser.majorVersion === 11) { if (!mathUtil.isPowerOfTwo(imageWidth)) { for (let i = 1; i < maxCubeMapTextureSize; i *= 2) { if (i < imageWidth) { continue; } else { imageWidth = i; break; } } } } if (agentInfo.os.name === "ios") { const majorVersion = agentInfo.os.majorVersion; // ios 9 의 경우 텍스쳐 최대사이즈는 1024 이다. if (majorVersion === 9) { imageWidth = 1024; } // ios 8 의 경우 텍스쳐 최대사이즈는 512 이다. if (majorVersion === 8) { imageWidth = 512; } } // maxCubeMapTextureSize 보다는 작고, imageWidth 보다 큰 2의 승수 중 가장 작은 수 return Math.min(maxCubeMapTextureSize, imageWidth); } private _shrinkCoord(coordData: { image: HTMLImageElement | HTMLVideoElement; faceCoords: number[][]; trim: number; }) { const { image, faceCoords, trim } = coordData; const inputTextureSize = Array.isArray(image) ? this.getDimension(image[0]).width : this.getSourceTileSize(image); // Shrink by "trim" px const SHRINK_MULTIPLIER = 1 - trim * (2 / inputTextureSize); const axisMultipliers = [0, 1, 2].map(axisIndex => { const axisDir = mathUtil.sign(faceCoords[0][axisIndex]); const notSameDir = faceCoords.some(coord => mathUtil.sign(coord[axisIndex]) !== axisDir); return notSameDir; }).map(notSameDir => notSameDir ? SHRINK_MULTIPLIER : 1); return faceCoords.map(coords => coords.map((coord, axisIndex) => coord * axisMultipliers[axisIndex])); } } export default CubeRenderer;