diff --git a/demos/02_contour.html b/demos/02_contour.html index b2782dde8c..3b177a5dbe 100644 --- a/demos/02_contour.html +++ b/demos/02_contour.html @@ -30,8 +30,7 @@ const scene = new L7.Scene({ zoom: 14.82, }); scene.on('loaded', () => { - $.get('./data/contour.geojson', data => { - // data.features = data.features.slice(0,1); + $.get('https://gw.alipayobjects.com/os/rmsportal/ZVfOvhVCzwBkISNsuKCc.json', data => { scene.LineLayer({ zIndex: 2 }) diff --git a/demos/dashline.html b/demos/dashline.html new file mode 100644 index 0000000000..537388f68c --- /dev/null +++ b/demos/dashline.html @@ -0,0 +1,55 @@ + + + + + + + + + dashline demo + + + + +
+ + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 43486efeb6..5e4095f1c6 100755 --- a/package.json +++ b/package.json @@ -108,8 +108,9 @@ "earcut": "^2.1.3", "fecha": "^2.3.3", "gl-matrix": "^2.4.1", + "gl-vec2": "^1.3.0", "lodash": "^4.17.5", - "polyline-normals": "^2.0.2", + "polyline-miter-util": "^1.0.1", "rbush": "^2.0.2", "simple-statistics": "^7.0.1", "supercluster": "^6.0.1", diff --git a/src/geom/buffer/line.js b/src/geom/buffer/line.js index b86dac7895..e5b3ed7932 100644 --- a/src/geom/buffer/line.js +++ b/src/geom/buffer/line.js @@ -75,7 +75,7 @@ export default class LineBuffer extends BufferBase { } _getMeshLineAttributes() { const layerData = this.get('layerData'); - const { lineType } = this.get('style'); + const { dashArray } = this.get('style'); const positions = []; const pickingIds = []; const normal = []; @@ -84,10 +84,11 @@ export default class LineBuffer extends BufferBase { const indexArray = []; const sizes = []; const attrDistance = []; + const attrDashArray = []; layerData.forEach(item => { const props = item; const positionCount = positions.length / 3; - const attr = lineShape.Line(item.coordinates, props, positionCount, (lineType !== 'soild')); + const attr = lineShape.Line(item.coordinates, props, positionCount, dashArray); positions.push(...attr.positions); normal.push(...attr.normal); miter.push(...attr.miter); @@ -96,6 +97,7 @@ export default class LineBuffer extends BufferBase { sizes.push(...attr.sizes); attrDistance.push(...attr.attrDistance); pickingIds.push(...attr.pickingIds); + attrDashArray.push(...attr.dashArray); }); return { positions, @@ -105,7 +107,8 @@ export default class LineBuffer extends BufferBase { indexArray, pickingIds, sizes, - attrDistance + attrDistance, + attrDashArray }; } diff --git a/src/geom/buffer/point/fillBuffer.js b/src/geom/buffer/point/fillBuffer.js index c2e861eaf6..a61a5a8ce4 100644 --- a/src/geom/buffer/point/fillBuffer.js +++ b/src/geom/buffer/point/fillBuffer.js @@ -29,6 +29,12 @@ export default function fillBuffer(layerData) { throw new Error('Invalid shape type: ' + shape); } toPointShapeAttributes(polygon, coordinates, { size, shape, color, id }, attribute); + // toPointShapeAttributes(polygon, null, {}, attribute); + // instanced attributes + // attribute.vertices.push(...coordinates); + // attribute.a_size.push(...size); + // attribute.colors.push(...color); + // attribute.pickingIds.push(id); }); return attribute; @@ -78,5 +84,8 @@ function toPointShapeAttributes(polygon, geo, style, attribute) { attribute.colors.push(...color, ...color, ...color); attribute.pickingIds.push(id, id, id); + // attribute.shapePositions.push(ax, ay, az, bx, by, bz, cx, cy, cz); + // attribute.normals.push(nx, ny, nz, nx, ny, nz, nx, ny, nz); + } } diff --git a/src/geom/material/lineMaterial.js b/src/geom/material/lineMaterial.js index 91a1c4d204..858a4b93e7 100644 --- a/src/geom/material/lineMaterial.js +++ b/src/geom/material/lineMaterial.js @@ -1,9 +1,9 @@ import * as THREE from '../../core/three'; import Material from './material'; -import { getModule } from '../../util/shaderModule'; +import { getModule, wrapUniforms } from '../../util/shaderModule'; import arcline_frag from '../shader/arcline_frag.glsl'; import arcline_vert from '../shader/arcline_vert.glsl'; - +import merge from '@antv/util/lib/deep-mix'; export function LineMaterial(options) { const { vs, fs } = getModule('line'); @@ -40,19 +40,14 @@ export function ArcLineMaterial(options) { return material; } -export function MeshLineMaterial(options) { - const { vs, fs } = getModule('meshline'); +export function MeshLineMaterial(options, defines) { + const { vs, fs, uniforms } = getModule('meshline'); const material = new Material({ - uniforms: { - u_opacity: { value: options.u_opacity || 1.0 }, - u_time: { value: options.u_time || 0 }, - u_zoom: { value: options.u_zoom }, - u_duration: { value: options.u_duration || 2.0 }, - u_interval: { value: options.u_interval || 1.0 }, - u_trailLength: { value: options.u_trailLength || 0.2 }, - u_activeId: { value: options.activeId || 0 }, - u_activeColor: { value: options.activeColor || [ 1.0, 0, 0, 1.0 ] } - }, + uniforms: wrapUniforms(merge(uniforms, options, { + u_activeId: options.activeId, + u_activeColor: options.activeColor + })), + defines, vertexShader: vs, fragmentShader: fs, transparent: true, @@ -60,23 +55,3 @@ export function MeshLineMaterial(options) { }); return material; } -export function DashLineMaterial(options) { - const { vs, fs } = getModule('meshline'); - const material = new Material({ - uniforms: { - u_opacity: { value: options.u_opacity || 1.0 }, - u_time: { value: options.u_time || 0 }, - u_zoom: { value: options.u_zoom }, - u_dashSteps: { value: options.u_dashSteps || 12 }, - u_dashSmooth: { value: options.u_dashSmooth || 0.01 }, - u_dashDistance: { value: options.u_dashDistance || 0.2 }, - u_activeId: { value: options.activeId || 0 }, - u_activeColor: { value: options.activeColor || [ 1.0, 0, 0, 1.0 ] } - }, - vertexShader: vs, - fragmentShader: fs, - transparent: true - }); - return material; -} - diff --git a/src/geom/shader/dashline_frag.glsl b/src/geom/shader/dashline_frag.glsl deleted file mode 100644 index 1cc8f95356..0000000000 --- a/src/geom/shader/dashline_frag.glsl +++ /dev/null @@ -1,10 +0,0 @@ -varying float v_lineU; -uniform float u_dashSteps; -uniform float u_dashSmooth; -uniform float u_dashDistance; -varying vec4 v_color; -void main() { - float lineUMod = mod(v_lineU, 1.0/ u_dashSteps) * u_dashSteps; - float dash = smoothstep(u_dashDistance, u_dashDistance+u_dashSmooth, length(lineUMod-0.5)); - gl_FragColor = vec4(v_color.xyz * vec3(dash), v_color.a * dash); -} \ No newline at end of file diff --git a/src/geom/shader/dashline_vert.glsl b/src/geom/shader/dashline_vert.glsl deleted file mode 100644 index d5492bd848..0000000000 --- a/src/geom/shader/dashline_vert.glsl +++ /dev/null @@ -1,23 +0,0 @@ -precision highp float; -attribute float a_miter; -attribute vec4 a_color; -attribute float a_size; -uniform float u_zoom; -uniform float u_opacity; -varying vec4 v_color; -attribute float a_distance; -varying float v_lineU; -uniform float u_activeId; -uniform vec4 u_activeColor; -void main() { - v_lineU = a_distance; - mat4 matModelViewProjection = projectionMatrix * modelViewMatrix; - vec3 pointPos = position.xyz + vec3(normal * a_size * pow(2.0,20.0-u_zoom) / 2.0 * a_miter); - v_color = a_color; - v_color.a *= u_opacity; - if(pickingId == u_activeId) { - v_color = u_activeColor; - } - gl_Position = matModelViewProjection * vec4(pointPos, 1.0); - worldId = id_toPickColor(pickingId); -} \ No newline at end of file diff --git a/src/geom/shader/index.js b/src/geom/shader/index.js index 0962214754..23e47bf8df 100644 --- a/src/geom/shader/index.js +++ b/src/geom/shader/index.js @@ -19,10 +19,6 @@ import mesh_line_vert from '../shader/meshline_vert.glsl'; import line_frag from '../shader/line_frag.glsl'; import line_vert from '../shader/line_vert.glsl'; -// 虚线 -import dash_vert from '../shader/dashline_vert.glsl'; -import dash_frag from '../shader/dashline_frag.glsl'; - // 热力图 import heatmap_color_vert from '../shader/heatmap_colorize_vert.glsl'; import heatmap_color_frag from '../shader/heatmap_colorize_frag.glsl'; @@ -58,7 +54,6 @@ export function compileBuiltinModules() { registerModule('pointline', { vs: point_line_vert, fs: point_line_frag }); registerModule('meshline', { vs: mesh_line_vert, fs: mesh_line_frag }); registerModule('line', { vs: line_vert, fs: line_frag }); - registerModule('dashline', { vs: dash_vert, fs: dash_frag }); registerModule('heatmap_color', { vs: heatmap_color_vert, fs: heatmap_color_frag }); registerModule('heatmap_intensity', { vs: heatmap_intensity_vert, fs: heatmap_intensity_frag }); registerModule('text', { vs: text_vert, fs: text_frag }); diff --git a/src/geom/shader/meshline_frag.glsl b/src/geom/shader/meshline_frag.glsl index 3372d3c248..bb96ae0092 100644 --- a/src/geom/shader/meshline_frag.glsl +++ b/src/geom/shader/meshline_frag.glsl @@ -1,11 +1,28 @@ precision highp float; -uniform float u_opacity; + +uniform float u_opacity : 1.0; +uniform float u_dash_offset : 0.0; +uniform float u_dash_ratio : 0.0; +uniform float u_blur : 0.9; + varying vec4 v_color; -varying float vTime; +varying float v_distance; +varying float v_dash_array; +varying float v_time; +varying vec2 v_normal; + void main() { - gl_FragColor = v_color; + gl_FragColor = v_color; + #ifdef DASHLINE + gl_FragColor.a *= u_opacity * ceil(mod(v_distance + u_dash_offset, v_dash_array) - (v_dash_array * u_dash_ratio)); + #else gl_FragColor.a = v_color.a * u_opacity; - #ifdef ANIMATE - gl_FragColor.a *= vTime; - #endif + #endif + #ifdef ANIMATE + gl_FragColor.a *= v_time; + #endif + + // anti-alias + float blur = 1. - smoothstep(u_blur, 1., length(v_normal)); + gl_FragColor.a *= blur; } diff --git a/src/geom/shader/meshline_vert.glsl b/src/geom/shader/meshline_vert.glsl index 76c5d8b73a..2a5a87aefe 100644 --- a/src/geom/shader/meshline_vert.glsl +++ b/src/geom/shader/meshline_vert.glsl @@ -1,38 +1,48 @@ -precision highp float; attribute float a_miter; attribute vec4 a_color; attribute float a_size; attribute float a_distance; +attribute float a_dash_array; + uniform float u_zoom; +uniform float u_time : 0; +uniform float u_activeId : 1; +uniform vec4 u_activeColor : [ 1.0, 0, 0, 1.0 ]; + +varying float v_time; varying vec4 v_color; -uniform float u_time; -varying float vTime; -uniform float u_activeId; -uniform vec4 u_activeColor; -// animate +varying float v_distance; +varying float v_dash_array; +varying vec2 v_normal; + #ifdef ANIMATE -uniform float u_duration; // 动画持续时间 -uniform float u_interval; -uniform float u_repeat; -uniform float u_trailLength; +uniform float u_duration : 2.0; +uniform float u_interval : 1.0; +uniform float u_trailLength : 0.2; #endif - void main() { - mat4 matModelViewProjection = projectionMatrix * modelViewMatrix; - vec3 pointPos = vec3(position.xy,0.) + vec3(normal * a_size * pow(2.0,20.0-u_zoom) / 2.0 * a_miter); - v_color = a_color; - if(pickingId == u_activeId) { + v_color = a_color; + v_distance = a_distance; + v_dash_array = a_dash_array; + + // anti-alias + v_normal = vec2(normal * sign(a_miter)); + + // extrude along normal + float extrude_scale = pow(2.0, 20.0 - u_zoom); + vec3 offset = vec3(normal * a_size * extrude_scale / 2.0 * a_miter); + gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xy + offset.xy, 0., 1.0); + + #ifdef ANIMATE + float alpha =1.0 - fract( mod(1.0- a_distance,u_interval)* (1.0/u_interval) + u_time / u_duration); + alpha = (alpha + u_trailLength -1.0) / u_trailLength; + v_time = clamp(alpha,0.,1.); + #endif + + // picking + if(pickingId == u_activeId) { v_color = u_activeColor; } - #ifdef ANIMATE - //mod(a_distance,0.2) * 5. - float alpa =1.0 - fract( mod(1.0- a_distance,u_interval)* (1.0/u_interval) + u_time / u_duration); - alpa = (alpa + u_trailLength -1.0) / u_trailLength; - vTime = clamp(alpa,0.,1.); - // vTime = (28800. + mod(u_time* 1000.,28800.)- position.z) / 100.; - #endif - worldId = id_toPickColor(pickingId); - gl_Position = matModelViewProjection * vec4(pointPos.xy, 0., 1.0); - + worldId = id_toPickColor(pickingId); } \ No newline at end of file diff --git a/src/geom/shape/line.js b/src/geom/shape/line.js index f171c1f86c..c3849a8f8d 100644 --- a/src/geom/shape/line.js +++ b/src/geom/shape/line.js @@ -1,4 +1,5 @@ -import getNormal from 'polyline-normals'; +import getNormals from '../../util/polyline-normals'; +import flatten from '@antv/util/lib/flatten'; /** * shape arc @@ -64,69 +65,61 @@ export function defaultLine(geo, index) { return { positions, indexes: indexArray }; } // mesh line -export function Line(path, props, positionsIndex) { +export function Line(path, props, positionsIndex, lengthPerDashSegment = 200) { if (path.length === 1) path = path[0];// 面坐标转线坐标 const positions = []; const pickingIds = []; const normal = []; const miter = []; const colors = []; - const indexArray = []; - const normals = getNormal(path); + const dashArray = []; + + const { normals, attrIndex, attrPos } = getNormals(path, false, positionsIndex); + let attrDistance = []; const sizes = []; - let c = 0; - let index = positionsIndex; const { size, color, id } = props; - path.forEach((point, pointIndex, list) => { - const i = index; - colors.push(...color); + attrPos.forEach((point, pointIndex) => { colors.push(...color); pickingIds.push(id); - pickingIds.push(id); sizes.push(size[0]); - sizes.push(size[0]); - if (pointIndex !== list.length - 1) { - indexArray[c++] = i + 0; - indexArray[c++] = i + 3; - indexArray[c++] = i + 1; - indexArray[c++] = i + 0; - indexArray[c++] = i + 2; - indexArray[c++] = i + 3; - } - // point[2] = size[1]; - positions.push(...point); + point[2] = size[1]; positions.push(...point); - if (pointIndex === 0) { - attrDistance.push(0, 0); + if (pointIndex === 0 || pointIndex === 1) { + attrDistance.push(0); + } else if (pointIndex % 2 === 0) { + attrDistance.push(attrDistance[pointIndex - 2] + + lineSegmentDistance(attrPos[pointIndex - 2], attrPos[pointIndex])); } else { - const d = attrDistance[pointIndex * 2 - 1] + lineSegmentDistance(path[pointIndex - 1], path[pointIndex]); - attrDistance.push(d, d); + attrDistance.push(attrDistance[pointIndex - 1]); } - - index += 2; }); + + const totalLength = attrDistance[attrDistance.length - 1]; + const ratio = lengthPerDashSegment / totalLength; normals.forEach(n => { const norm = n[0]; const m = n[1]; normal.push(norm[0], norm[1], 0); - normal.push(norm[0], norm[1], 0); - miter.push(-m); miter.push(m); + dashArray.push(ratio); }); + attrDistance = attrDistance.map(d => { - return d / attrDistance[attrDistance.length - 1]; + return d / totalLength; }); + return { positions, normal, - indexArray, + indexArray: flatten(attrIndex), miter, colors, sizes, pickingIds, - attrDistance + attrDistance, + dashArray }; } diff --git a/src/layer/render/line/drawDashLine.js b/src/layer/render/line/drawDashLine.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/layer/render/line/drawMeshLine.js b/src/layer/render/line/drawMeshLine.js index 37eaf2ad25..5709c5ae77 100644 --- a/src/layer/render/line/drawMeshLine.js +++ b/src/layer/render/line/drawMeshLine.js @@ -11,15 +11,19 @@ export default function DrawLine(attributes, cfg, layer) { geometry.addAttribute('normal', new THREE.Float32BufferAttribute(attributes.normal, 3)); geometry.addAttribute('a_miter', new THREE.Float32BufferAttribute(attributes.miter, 1)); geometry.addAttribute('a_distance', new THREE.Float32BufferAttribute(attributes.attrDistance, 1)); + geometry.addAttribute('a_dash_array', new THREE.Float32BufferAttribute(attributes.attrDashArray, 1)); const lineMaterial = new MeshLineMaterial({ u_opacity: style.opacity, u_zoom: zoom, u_time: 0, + u_dash_offset: style.dashOffset, + u_dash_ratio: style.dashRatio, activeColor: activeOption.fill }, { SHAPE: false, - ANIMATE: false + ANIMATE: false, + DASHLINE: style.lineType === 'dash' }); const lineMesh = new THREE.Mesh(geometry, lineMaterial); diff --git a/src/layer/render/point/drawFill.js b/src/layer/render/point/drawFill.js index 883d762107..008d47dd35 100644 --- a/src/layer/render/point/drawFill.js +++ b/src/layer/render/point/drawFill.js @@ -16,6 +16,17 @@ export default function DrawFill(attributes, style) { geometry.addAttribute('normal', new THREE.Float32BufferAttribute(attributes.normals, 3)); geometry.addAttribute('a_shape', new THREE.Float32BufferAttribute(attributes.shapePositions, 3)); geometry.addAttribute('a_size', new THREE.Float32BufferAttribute(attributes.a_size, 3)); + + // const instancedGeometry = new THREE.InstancedBufferGeometry(); + + // instancedGeometry.addAttribute('normal', new THREE.Float32BufferAttribute(attributes.normals, 3)); + // instancedGeometry.addAttribute('a_shape', new THREE.Float32BufferAttribute(attributes.shapePositions, 3)); + // // instanced attributes + // instancedGeometry.addAttribute('position', new THREE.InstancedBufferAttribute(new Float32Array(attributes.vertices), 3)); + // instancedGeometry.addAttribute('a_color', new THREE.InstancedBufferAttribute(new Float32Array(attributes.colors), 4)); + // instancedGeometry.addAttribute('pickingId', new THREE.InstancedBufferAttribute(new Float32Array(attributes.pickingIds), 1)); + // instancedGeometry.addAttribute('a_size', new THREE.InstancedBufferAttribute(new Float32Array(attributes.a_size), 3)); + const material = new PolygonMaterial({ u_opacity: opacity, u_activeColor: activeColor @@ -25,6 +36,7 @@ export default function DrawFill(attributes, style) { material.setDefinesvalue('SHAPE', true); material.depthTest = false; const fillMesh = new THREE.Mesh(geometry, material); + // const fillMesh = new THREE.Mesh(instancedGeometry, material); return fillMesh; } diff --git a/src/util/polyline-normals.js b/src/util/polyline-normals.js new file mode 100644 index 0000000000..01b649903c --- /dev/null +++ b/src/util/polyline-normals.js @@ -0,0 +1,131 @@ +/** + * 对于 polyline-normal 的改进 + * 超过阈值,miter 转成 bevel 接头, + * 要注意 Three.js 中默认 THREE.FrontFaceDirectionCCW + * @see https://zhuanlan.zhihu.com/p/59541559 + */ +import { direction, normal, computeMiter } from 'polyline-miter-util'; +import { create, copy, dot } from 'gl-vec2'; + +function extrusions(positions, out, point, normal, scale) { + addNext(out, normal, -scale); + addNext(out, normal, scale); + positions.push(point); + positions.push(point); +} + +function addNext(out, normal, length) { + out.push([[ normal[0], normal[1] ], length ]); +} + +export default function(points, closed, indexOffset) { + const lineA = [ 0, 0 ]; + const lineB = [ 0, 0 ]; + const tangent = [ 0, 0 ]; + const miter = [ 0, 0 ]; + let _lastFlip = -1; + let _started = false; + let _normal = null; + const tmp = create(); + let count = indexOffset || 0; + const miterLimit = 3; + + const out = []; + const attrPos = []; + const attrIndex = []; + const attrCounters = [ 0, 0 ]; + if (closed) { + points = points.slice(); + points.push(points[0]); + } + + const total = points.length; + + for (let i = 1; i < total; i++) { + const index = count; + const last = points[i - 1]; + const cur = points[i]; + const next = i < points.length - 1 ? points[i + 1] : null; + + attrCounters.push(i / total, i / total); + + direction(lineA, cur, last); + + if (!_normal) { + _normal = [ 0, 0 ]; + normal(_normal, lineA); + } + + if (!_started) { + _started = true; + extrusions(attrPos, out, last, _normal, 1); + } + + attrIndex.push([ index + 0, index + 3, index + 1 ]); + + // no miter, simple segment + if (!next) { + // reset normal + normal(_normal, lineA); + extrusions(attrPos, out, cur, _normal, 1); + attrIndex.push( + _lastFlip === 1 ? [ index + 1, index + 3, index + 2 ] : [ index, index + 2, index + 3 ]); + + count += 2; + } else { + // get unit dir of next line + direction(lineB, next, cur); + + // stores tangent & miter + let miterLen = computeMiter(tangent, miter, lineA, lineB, 1); + + // get orientation + let flip = (dot(tangent, _normal) < 0) ? -1 : 1; + const bevel = miterLen > miterLimit; + if (bevel) { + miterLen = miterLimit; + attrCounters.push(i / total); + + // next two points in our first segment + addNext(out, _normal, -flip); + attrPos.push(cur); + addNext(out, miter, miterLen * flip); + attrPos.push(cur); + + attrIndex.push(_lastFlip !== -flip + ? [ index + 1, index + 3, index + 2 ] : [ index, index + 2, index + 3 ]); + + // now add the bevel triangle + attrIndex.push([ index + 2, index + 3, index + 4 ]); + + normal(tmp, lineB); + copy(_normal, tmp); // store normal for next round + + addNext(out, _normal, -flip); + attrPos.push(cur); + + // the miter is now the normal for our next join + count += 3; + } else { + // next two points for our miter join + extrusions(attrPos, out, cur, miter, miterLen); + attrIndex.push(_lastFlip === 1 + ? [ index + 1, index + 3, index + 2 ] : [ index, index + 2, index + 3 ]); + + flip = -1; + + // the miter is now the normal for our next join + copy(_normal, miter); + count += 2; + } + _lastFlip = flip; + } + } + + return { + normals: out, + attrIndex, + attrPos, + attrCounters + }; +} diff --git a/src/util/shaderModule.js b/src/util/shaderModule.js index b73abed272..2c6c775d85 100644 --- a/src/util/shaderModule.js +++ b/src/util/shaderModule.js @@ -1,3 +1,7 @@ +import uniq from '@antv/util/lib/uniq'; +import isString from '@antv/util/lib/is-string'; +import ColorUtil from '../attr/color-util'; + const SHADER_TYPE = { VS: 'vs', FS: 'fs' @@ -9,9 +13,10 @@ const globalDefaultprecision = '#ifdef GL_FRAGMENT_PRECISION_HIGH\n precision hi const globalDefaultAttribute = 'attribute float pickingId;\n varying vec4 worldId;\n'; const globalDefaultInclude = '#pragma include "pick_color"\n'; const includeRegExp = /#pragma include (["^+"]?["\ "[a-zA-Z_0-9](.*)"]*?)/g; +const uniformRegExp = /uniform\s+(bool|float|int|vec2|vec3|vec4|ivec2|ivec3|ivec4|mat2|mat3|mat4|sampler2D|samplerCube)\s+([\s\S]*?);/g; function processModule(rawContent, includeList, type) { - return rawContent.replace(includeRegExp, (_, strMatch) => { + const compiled = rawContent.replace(includeRegExp, (_, strMatch) => { const includeOpt = strMatch.split(' '); const includeName = includeOpt[0].replace(/"/g, ''); @@ -19,18 +24,110 @@ function processModule(rawContent, includeList, type) { return ''; } - let txt = rawContentCache[includeName][type]; + const txt = rawContentCache[includeName][type]; includeList.push(includeName); - txt = processModule(txt, includeList, type); - return txt; + const { content } = processModule(txt, includeList, type); + return content; }); + + return { + content: compiled, + includeList + }; } -export function registerModule(moduleName, { vs, fs }) { +function getUniformLengthByType(type) { + let arrayLength = 0; + switch (type) { + case 'vec2': + case 'ivec2': + arrayLength = 2; + break; + case 'vec3': + case 'ivec3': + arrayLength = 3; + break; + case 'vec4': + case 'ivec4': + case 'mat2': + arrayLength = 4; + break; + case 'mat3': + arrayLength = 9; + break; + case 'mat4': + arrayLength = 16; + break; + default: + } + return arrayLength; +} + +function extractUniforms(content) { + const uniforms = {}; + content = content.replace(uniformRegExp, (_, type, c) => { + const defaultValues = c.split(':'); + const uniformName = defaultValues[0].trim(); + let defaultValue = ''; + if (defaultValues.length > 1) { + defaultValue = defaultValues[1].trim(); + } + + // set default value for uniform according to its type + // eg. vec2 u -> [0.0, 0.0] + switch (type) { + case 'bool': + defaultValue = defaultValue === 'true'; + break; + case 'float': + case 'int': + defaultValue = Number(defaultValue); + break; + case 'vec2': + case 'vec3': + case 'vec4': + case 'ivec2': + case 'ivec3': + case 'ivec4': + case 'mat2': + case 'mat3': + case 'mat4': + if (defaultValue) { + defaultValue = defaultValue.replace('[', '').replace(']', '') + .split(',') + .reduce((prev, cur) => { + prev.push(Number(cur.trim())); + return prev; + }, []); + } else { + defaultValue = new Array(getUniformLengthByType(type)).fill(0); + } + break; + default: + } + + uniforms[uniformName] = defaultValue; + return `uniform ${type} ${uniformName};\n`; + }); + return { + content, + uniforms + }; +} + +export function registerModule(moduleName, { vs, fs, uniforms: declaredUniforms }) { + const { content: extractedVS, uniforms: vsUniforms } = extractUniforms(vs); + const { content: extractedFS, uniforms: fsUniforms } = extractUniforms(fs); + rawContentCache[moduleName] = { - [SHADER_TYPE.VS]: vs, - [SHADER_TYPE.FS]: fs + [SHADER_TYPE.VS]: extractedVS, + [SHADER_TYPE.FS]: extractedFS, + uniforms: { + ...vsUniforms, + ...fsUniforms, + ...declaredUniforms + } }; } @@ -39,11 +136,20 @@ export function getModule(moduleName) { return moduleCache[moduleName]; } - let vs = rawContentCache[moduleName][SHADER_TYPE.VS]; - let fs = rawContentCache[moduleName][SHADER_TYPE.FS]; - vs = globalDefaultAttribute + globalDefaultInclude + vs; - vs = processModule(vs, [], SHADER_TYPE.VS); - fs = processModule(fs, [], SHADER_TYPE.FS); + let rawVS = rawContentCache[moduleName][SHADER_TYPE.VS]; + const rawFS = rawContentCache[moduleName][SHADER_TYPE.FS]; + + rawVS = globalDefaultAttribute + globalDefaultInclude + rawVS; + + const { content: vs, includeList: vsIncludeList } = processModule(rawVS, [], SHADER_TYPE.VS); + let { content: fs, includeList: fsIncludeList } = processModule(rawFS, [], SHADER_TYPE.FS); + // TODO: extract uniforms and their default values from GLSL + const uniforms = uniq(vsIncludeList.concat(fsIncludeList).concat(moduleName)).reduce((prev, cur) => { + return { + ...prev, + ...rawContentCache[cur].uniforms + }; + }, {}); /** * set default precision for fragment shader @@ -55,7 +161,76 @@ export function getModule(moduleName) { moduleCache[moduleName] = { [SHADER_TYPE.VS]: vs.trim(), - [SHADER_TYPE.FS]: fs.trim() + [SHADER_TYPE.FS]: fs.trim(), + uniforms }; return moduleCache[moduleName]; } + +export function wrapUniforms(uniforms) { + return Object.keys(uniforms).reduce((prev, cur) => { + prev[cur] = { + value: uniforms[cur] + }; + return prev; + }, {}); +} + +const DEFAULT_LIGHT = { + type: 'directional', + direction: [ 1, 10.5, 12 ], + ambient: [ 0.2, 0.2, 0.2 ], + diffuse: [ 0.6, 0.6, 0.6 ], + specular: [ 0.1, 0.1, 0.1 ] +}; + +const DEFAULT_DIRECTIONAL_LIGHT = { + direction: [ 0, 0, 0 ], + ambient: [ 0, 0, 0 ], + diffuse: [ 0, 0, 0 ], + specular: [ 0, 0, 0 ] +}; + +const DEFAULT_SPOT_LIGHT = { + position: [ 0, 0, 0 ], + direction: [ 0, 0, 0 ], + ambient: [ 0, 0, 0 ], + diffuse: [ 0, 0, 0 ], + specular: [ 0, 0, 0 ], + constant: 1, + linear: 0, + quadratic: 0, + angle: 14, + exponent: 40, + blur: 5 +}; + +const COLOR_ATTRIBUTES = [ + 'ambient', 'diffuse', 'specular' +]; + +export function generateLightingUniforms(lights) { + const lightsMap = { + u_directional_lights: new Array(3).fill({ ...DEFAULT_DIRECTIONAL_LIGHT }), + u_num_of_directional_lights: 0, + u_spot_lights: new Array(3).fill({ ...DEFAULT_SPOT_LIGHT }), + u_num_of_spot_lights: 0 + }; + if (!lights || !lights.length) { + lights = [ DEFAULT_LIGHT ]; + } + lights.forEach(({ type, ...rest }, i) => { + const lightsUniformName = `u_${type}_lights`; + const lightsNumUniformName = `u_num_of_${type}_lights`; + + Object.keys(rest).forEach(key => { + if (isString(rest[key]) && COLOR_ATTRIBUTES.indexOf(key) > -1) { + rest[key] = ColorUtil.color2RGBA(rest[key]).slice(0, 3); + } + }); + + lightsMap[lightsUniformName][i] = { ...lightsMap[lightsUniformName][i], ...rest }; + lightsMap[lightsNumUniformName]++; + }); + return lightsMap; +}