UNPKG

tangram

Version:
374 lines (298 loc) 13.1 kB
import ShaderProgram from '../gl/shader_program'; import GLSL from '../gl/glsl'; import Geo from '../utils/geo'; import Vector from '../utils/vector'; import StyleParser from '../styles/style_parser'; import ambient_source from './ambient_light.glsl'; import directional_source from './directional_light.glsl'; import point_source from './point_light.glsl'; import spot_source from './spot_light.glsl'; // Abstract light export default class Light { constructor (view, config) { this.name = config.name; this.view = view; if (config.ambient == null || typeof config.ambient === 'number') { this.ambient = GLSL.expandVec3(config.ambient || 0); } else { this.ambient = StyleParser.parseColor(config.ambient).slice(0, 3); } if (config.diffuse == null || typeof config.diffuse === 'number') { this.diffuse = GLSL.expandVec3(config.diffuse != null ? config.diffuse : 1); } else { this.diffuse = StyleParser.parseColor(config.diffuse).slice(0, 3); } if (config.specular == null || typeof config.specular === 'number') { this.specular = GLSL.expandVec3(config.specular || 0); } else { this.specular = StyleParser.parseColor(config.specular).slice(0, 3); } } // Create a light by type name, factory-style // 'config' must include 'name' and 'type', along with any other type-specific properties static create (view, config) { if (Light.types[config.type]) { return new Light.types[config.type](view, config); } } // Set light for a style: fragment lighting, vertex lighting, or none static setMode (mode, style) { if (mode === true) { mode = 'fragment'; } mode = Light.enabled && ((mode != null) ? mode : 'fragment'); // default to fragment lighting style.defines['TANGRAM_LIGHTING_FRAGMENT'] = (mode === 'fragment'); style.defines['TANGRAM_LIGHTING_VERTEX'] = (mode === 'vertex'); } // Inject all provided light definitions, and calculate cumulative light function static inject (lights) { // Clear previous injections ShaderProgram.removeBlock(Light.block); // If lighting is globally disabled, nothing is injected (mostly for debugging or live editing) if (!Light.enabled) { return; } // Construct code to calculate each light instance let calculateLights = ''; if (lights && Object.keys(lights).length > 0) { // Collect uniques types of lights let types = {}; for (let light_name in lights) { types[lights[light_name].type] = true; } // Inject each type of light for (let type in types) { Light.types[type].inject(); } // Inject per-instance blocks and construct the list of functions to calculate each light for (let light_name in lights) { // Define instance lights[light_name].inject(); // Add the calculation function to the list calculateLights += `calculateLight(${light_name}, _eyeToPoint, _normal);\n`; } } // Glue together the final lighting function that sums all the lights let calculateFunction = ` vec4 calculateLighting(in vec3 _eyeToPoint, in vec3 _normal, in vec4 _color) { // Do initial material calculations over normal, emission, ambient, diffuse and specular values calculateMaterial(_eyeToPoint,_normal); // Un roll the loop of individual ligths to calculate ${calculateLights} // Final light intensity calculation vec4 color = vec4(vec3(0.), _color.a); // start with vertex color alpha #ifdef TANGRAM_MATERIAL_EMISSION color.rgb = material.emission.rgb; color.a *= material.emission.a; #endif #ifdef TANGRAM_MATERIAL_AMBIENT color.rgb += light_accumulator_ambient.rgb * _color.rgb * material.ambient.rgb; color.a *= material.ambient.a; #else #ifdef TANGRAM_MATERIAL_DIFFUSE color.rgb += light_accumulator_ambient.rgb * _color.rgb * material.diffuse.rgb; #endif #endif #ifdef TANGRAM_MATERIAL_DIFFUSE color.rgb += light_accumulator_diffuse.rgb * _color.rgb * material.diffuse.rgb; color.a *= material.diffuse.a; #endif #ifdef TANGRAM_MATERIAL_SPECULAR color.rgb += light_accumulator_specular.rgb * material.specular.rgb; color.a *= material.specular.a; #endif // Clamp final color color = clamp(color, 0.0, 1.0); return color; }`; ShaderProgram.addBlock(Light.block, calculateFunction); } // Common instance definition inject () { let instance = ` uniform ${this.struct_name} u_${this.name}; ${this.struct_name} ${this.name}; `; let assign = ` ${this.name} = u_${this.name};\n `; ShaderProgram.addBlock(Light.block, instance); ShaderProgram.addBlock('setup', assign); } // Update method called once per frame update () { } // Called once per frame per program (e.g. for main render pass, then for each additional // pass for feature selection, etc.) setupProgram (_program) { // Three common light properties _program.uniform('3fv', `u_${this.name}.ambient`, this.ambient); _program.uniform('3fv', `u_${this.name}.diffuse`, this.diffuse); _program.uniform('3fv', `u_${this.name}.specular`, this.specular); } } Light.types = {}; // references to subclasses by short name Light.block = 'lighting'; // shader block name Light.enabled = true; // lighting can be globally enabled/disabled // Light subclasses class AmbientLight extends Light { constructor(view, config) { super(view, config); this.type = 'ambient'; this.struct_name = 'AmbientLight'; } // Inject struct and calculate function static inject() { ShaderProgram.addBlock(Light.block, ambient_source); } setupProgram (_program) { _program.uniform('3fv', `u_${this.name}.ambient`, this.ambient); } } Light.types['ambient'] = AmbientLight; class DirectionalLight extends Light { constructor(view, config) { super(view, config); this.type = 'directional'; this.struct_name = 'DirectionalLight'; if (config.direction) { this._direction = config.direction; } else { // Default directional light maintains full intensity on ground, with basic extrusion shading let theta = 135; // angle of light in xy plane (rotated around z axis) let scale = Math.sin(Math.PI*60/180); // scaling factor to keep total directional intensity to 0.5 this._direction = [ Math.cos(Math.PI*theta/180) * scale, Math.sin(Math.PI*theta/180) * scale, -0.5 ]; if (config.ambient == null) { this.ambient = GLSL.expandVec3(0.5); } } this.direction = this._direction.map(parseFloat); } get direction () { return this._direction; } set direction (v) { this._direction = Vector.normalize(Vector.copy(v)); } // Inject struct and calculate function static inject() { ShaderProgram.addBlock(Light.block, directional_source); } setupProgram (_program) { super.setupProgram(_program); _program.uniform('3fv', `u_${this.name}.direction`, this.direction); } } Light.types['directional'] = DirectionalLight; class PointLight extends Light { constructor (view, config) { super(view, config); this.type = 'point'; this.struct_name = 'PointLight'; this.position = config.position || [0, 0, '100px']; this.position_eye = []; // position in eyespace this.origin = config.origin || 'ground'; this.attenuation = !isNaN(parseFloat(config.attenuation)) ? parseFloat(config.attenuation) : 0; if (config.radius) { if (Array.isArray(config.radius) && config.radius.length === 2) { this.radius = config.radius; } else { this.radius = [null, config.radius]; } } else { this.radius = null; } } // Inject struct and calculate function static inject () { ShaderProgram.addBlock(Light.block, point_source); } // Inject isntance-specific settings inject() { super.inject(); ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT'] = (this.attenuation !== 0); ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS'] = (this.radius != null && this.radius[0] != null); ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS'] = (this.radius != null); } update () { this.updateEyePosition(); } updateEyePosition () { if (this.origin === 'world') { // For world origin, format is: [longitude, latitude, meters (default) or pixels w/px units] // Move light's world position into camera space const m = Geo.latLngToMeters([...this.position]); this.position_eye[0] = m[0] - this.view.camera.position_meters[0]; this.position_eye[1] = m[1] - this.view.camera.position_meters[1]; this.position_eye[2] = StyleParser.convertUnits(this.position[2], { zoom: this.view.zoom, meters_per_pixel: Geo.metersPerPixel(this.view.zoom) }); this.position_eye[2] = this.position_eye[2] - this.view.camera.position_meters[2]; } else if (this.origin === 'ground' || this.origin === 'camera') { // For camera or ground origin, format is: [x, y, z] in meters (default) or pixels w/px units // Light is in camera space by default this.position_eye = StyleParser.convertUnits(this.position, { zoom: this.view.zoom, meters_per_pixel: Geo.metersPerPixel(this.view.zoom) }); if (this.origin === 'ground') { // Leave light's xy in camera space, but z needs to be moved relative to ground plane this.position_eye[2] = this.position_eye[2] - this.view.camera.position_meters[2]; } } this.position_eye[3] = 1; } setupProgram (_program) { super.setupProgram(_program); _program.uniform('4fv', `u_${this.name}.position`, this.position_eye); if(ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT']) { _program.uniform('1f', `u_${this.name}.attenuationExponent`, this.attenuation); } if(ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS']) { _program.uniform('1f', `u_${this.name}.innerRadius`, StyleParser.convertUnits(this.radius[0], { zoom: this.view.zoom, meters_per_pixel: Geo.metersPerPixel(this.view.zoom) })); } if(ShaderProgram.defines['TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS']) { _program.uniform('1f', `u_${this.name}.outerRadius`, StyleParser.convertUnits(this.radius[1], { zoom: this.view.zoom, meters_per_pixel: Geo.metersPerPixel(this.view.zoom) })); } } } Light.types['point'] = PointLight; class SpotLight extends PointLight { constructor (view, config) { super(view, config); this.type = 'spotlight'; this.struct_name = 'SpotLight'; this.direction = this._direction = (config.direction || [0, 0, -1]).map(parseFloat); // [x, y, z] this.exponent = config.exponent ? parseFloat(config.exponent) : 0.2; this.angle = config.angle ? parseFloat(config.angle) : 20; } get direction () { return this._direction; } set direction (v) { this._direction = Vector.normalize(Vector.copy(v)); } // Inject struct and calculate function static inject () { ShaderProgram.addBlock(Light.block, spot_source); } setupProgram (_program) { super.setupProgram(_program); _program.uniform('3fv', `u_${this.name}.direction`, this.direction); _program.uniform('1f', `u_${this.name}.spotCosCutoff`, Math.cos(this.angle * 3.14159 / 180)); _program.uniform('1f', `u_${this.name}.spotExponent`, this.exponent); } } Light.types['spotlight'] = SpotLight;