diff --git a/Docs/README.scenery b/Docs/README.scenery index 95ef7304f..a1a514657 100644 --- a/Docs/README.scenery +++ b/Docs/README.scenery @@ -21,6 +21,8 @@ Contents ---------------------------------------------------------------------- 3.9 BUILDING_LIST 3.10 TREE_LIST 3.11 LINEAR_FEATURE_LIST + 3.12 OBJECT_LIGHT + 3.13 LIGHT_LIST 4 model manager ("/models/model") 4.1 static objects @@ -528,6 +530,85 @@ Where: - A,B,C,D are float values used to store attributes (currently unused) - lonN, latN are lon/lat pairs on the center of the line feature. There must be at least two pairs +3.12 OBJECT_LIGHT +------------------ + +Add a light to the tile. + +Example: + + OBJECT_LIGHT sample-light -129.00074514 -9.00490134 5 10.0 100.0 0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 360.0 360.0 -1 1.0 1.0 0.0 + +Syntax: + + OBJECT_LIGHT + +Where: +- is a name for the light. Multiple lights may have the same name. +- and are positions in longitude and latitude of the light. +- is in meter and relative to mean sea-level (in the fgfs world). +- is the size in centimeters of the light. +- is the intensity of the light in candelas. +- defines what part of the day the light is turned on at: + 0 means the light will be on all the time + 1 means the light will turn on at sunset (sun angle ~89 degrees) + 2 means the light will turn on around sunset with some variability + 3 means the light will turn on at sunset or when visibility is less than 5000nm +- , , and define the color of the light. +- , , , and define the directionality of the light: + If both are 360, the light is considered omnidirectional and the normals have no effect. + If not, points to north, points to east and points up. +- , , and define animations: + If is less than 0, light is not animated. + If is greater than 0, then its value is used as the total time interval of a single loop of the animation + defines the portion of the loop the light is working (its switched off completely for the remaining portion) + defines the strobe rate of the light w.r.t. to the full loop interval (i.e. ). + A value less than equal to zero turns off strobing completely. + defines the offset (so two lights with the same animation can start at different real-world times) + + Examples: + - On for half a second, off for half a second + 1.0 0.5 0.0 0.0 + - Alternatively, + 1.0 1.0 1.0 0.0 + - On for half a second, off for half a second, offset by 0.2 seconds + 1.0 0.5 0.0 0.2 + - Blink twice for half a second, and then remain off for half a second + 1.0 0.5 4.0 0.0 + +3.13 LIGHT_LIST +---------------- +Defines a file containing multiple light coordinates and properties. + +Example: + + LIGHT_LIST light_list.txt -129.00074514 -9.00490134 5 + +Syntax + + LIGHT_LIST + +Where: +- is the name of a file containing light positions and properties +- , , defines the center of the set of lights + +The referenced (in the example light_list.txt) contains lines defining lights of various types + +- "omnidirectional": + X Y Z +- "omnidirectional-animated": + X Y Z +- "directional": + X Y Z +- "directional-animated": + X Y Z + +The type of the light is automatically detected based on the number of parameters specified for each light. + +Where: +- X,Y,Z are the cartesian coordinates of the tree. +X is South and +Y is East. +- The rest of the parameters are the same as OBJECT_LIGHT (Section 3.12) + 4 model manager ("/models/model") -------------------------------------------- diff --git a/Effects/scenery-lights.eff b/Effects/scenery-lights.eff new file mode 100644 index 000000000..b805837d6 --- /dev/null +++ b/Effects/scenery-lights.eff @@ -0,0 +1,296 @@ + + + Effects/scenery-lights + + + light-sprite + clamp + clamp + + /environment/ground-visibility-m + /environment/visibility-m + /environment/ground-haze-thickness-m + /sim/rendering/eye-altitude-m + /environment/terminator-relative-position-m + /sim/rendering/als-filters/use-night-vision + /sim/rendering/als-filters/use-IR-vision + /sim/time/sun-angle-rad + + + + + + + /sim/rendering/point-sprites + + /sim/rendering/shaders/skydome + /sim/rendering/shaders/use-shaders + + + + 2.0 + + + + GL_ARB_point_sprite + GL_ARB_point_parameters + GL_ARB_shader_objects + GL_ARB_shading_language_100 + GL_ARB_vertex_shader + GL_ARB_fragment_shader + + + + + + + 8 + DepthSortedBin + + false + + src-alpha + one-minus-src-alpha + + + false + + off + + point + point + + + 0 + true + texture[0]/type + texture[0]/wrap-s + texture[0]/wrap-t + + + Shaders/scenery-lights.vert + Shaders/scenery-lights.frag + Shaders/noise.frag + + lightParams + 11 + + + animationParams + 12 + + + directionParams1 + 13 + + + directionParams2 + 14 + + + + visibility + float + visibility + + + avisibility + float + avisibility + + + hazeLayerAltitude + float + lthickness + + + eye_alt + float + eye_alt + + + use_night_vision + bool + use_night_vision + + + use_IR_vision + bool + use_IR_vision + + + sun_angle + float + sun_angle + + + texture + sampler-2d + 0 + + + terminator + float + terminator + + true + + + + + + + + /sim/rendering/point-sprites + + + 2.0 + + + + GL_ARB_point_sprite + GL_ARB_point_parameters + + + + + + + 8 + DepthSortedBin + + false + + false + + + src-alpha + one-minus-src-alpha + + + gequal + 0.1 + + back + + point + + + min-size + max-size + size + attenuation + + + 0 + true + texture[0]/type + texture[0]/wrap-s + texture[0]/wrap-t + + + + + + + + + /sim/rendering/point-sprites + + + 2.0 + + + GL_ARB_point_sprite + + + + + + 8 + DepthSortedBin + + false + + false + + + src-alpha + one-minus-src-alpha + + cull-face + + point + + + 0 + true + texture[0]/type + texture[0]/wrap-s + texture[0]/wrap-t + + + + + + + + + + + 2.0 + + + GL_ARB_point_parameters + + + + + + min-size + max-size + size + attenuation + + + 8 + DepthSortedBin + + + false + + + src-alpha + one-minus-src-alpha + + false + cull-face + + point + point + + + + + + + + + 8 + DepthSortedBin + + false + + false + + + src-alpha + one-minus-src-alpha + + cull-face + + point + + + + diff --git a/Shaders/scenery-lights.frag b/Shaders/scenery-lights.frag new file mode 100644 index 000000000..ce9251de3 --- /dev/null +++ b/Shaders/scenery-lights.frag @@ -0,0 +1,174 @@ +// -*-C++-*- +#version 120 + +// Shader that takes a list of GL_POINTS and draws a light (point-sprite like +// texture, more accurately a light halo) at the given point. This shader +// provides support for light animations like blinking, time period handling +// for lights on only during night time or in low visiblity and directional +// lighting. +// +// The actual rendering code is heavily based on an existing implementation +// found at: +// FGData commit 9355d464c175bd5d51ba32527180ed4e94e86fbb +// Shaders/surface-lights-ALS.frag +// with minor modifications for readability and tuning. +// +// Licence: GPL v2+ +// Written by Fahim Dalvi, January 2021 + +uniform sampler2D texture; + +uniform float visibility; +uniform float avisibility; +uniform float hazeLayerAltitude; +uniform float eye_alt; +uniform float terminator; + +uniform bool use_IR_vision; +uniform bool use_night_vision; + +varying vec3 relativePosition; +varying vec2 rawPosition; +varying float apparentSize; +varying float haloSize; +varying float lightSize; +varying float lightIntensity; + +float alt; + +float Noise2D(in vec2 coord, in float wavelength); + +float fog_func (in float targ) +{ + float fade_mix; + + // for large altitude > 30 km, we switch to some component of quadratic distance fading to + // create the illusion of improved visibility range + targ = 1.25 * targ * smoothstep(0.04,0.06,targ); // need to sync with the distance to which terrain is drawn + if (alt < 30000.0) { + return exp(-targ - targ * targ * targ * targ); + } else if (alt < 50000.0) { + fade_mix = (alt - 30000.0)/20000.0; + return fade_mix * exp(-targ*targ - pow(targ,4.0)) + (1.0 - fade_mix) * exp(-targ - pow(targ,4.0)); + } else { + return exp(- targ * targ - pow(targ,4.0)); + } +} + + +float light_sprite (in vec2 coord, in float transmission, in float noise) +{ + // Center the texture coordinates at (0,0) + coord.s = coord.s - 0.5; + coord.t = coord.t - 0.5; + + // Radius of the current pixel from the center of the light ranging from 0 to 1 + float r = length(coord) * 2; + + // If the light is too small, return constant intensity + if (apparentSize<1.3) {return 0.08;} + + // Calculate the rays (star-shaped structure) around the light + // These are randomized for every light based on `noise` + float angle = noise * 6.2832; + float sinphi = dot(vec2 (sin(angle),cos(angle)), normalize(coord)); + float sinterm = sin(mod((sinphi-3.0) * (sinphi-3.0),6.2832)); + float ray = 0.0; + if (sinterm == 0.0) { + ray = 0.0; + } else { + ray = sinterm * sinterm * sinterm * sinterm * sinterm * sinterm * sinterm * sinterm * sinterm * sinterm; + } + ray *= 0.2 * exp(-4 * pow(r, 2.5)); + + float fogEffect = (1.0-smoothstep(0.4, 0.8, transmission)); + float halo = 0.2 * exp(-4.0 * pow(r, 2.5)); + float base = exp(-4 * pow(r * haloSize, 2.5)); + + // Combine: + // base: the central disc of the light + // halo: the faint discs around the light + // ray: star-like structures around the disk + float intensity = clamp(ray + base + halo, 0.0, 1.0) + 0.1 * fogEffect * (1.0-smoothstep(0.3, 0.6, r)); + + return intensity; +} + + +void main() +{ + float dist = length(relativePosition); + float delta_z = hazeLayerAltitude - eye_alt; + float transmission; + float vAltitude; + float delta_zv; + float H; + float distance_in_layer; + float transmission_arg; + + if (use_IR_vision) {discard;} + + float noise = Noise2D(rawPosition.xy ,1.0); + + // angle with horizon + float ct = dot(vec3(0.0, 0.0, 1.0), relativePosition)/dist; + + // we solve the geometry what part of the light path is attenuated normally and what is through the haze layer + if (delta_z > 0.0) // we're inside the layer + { + if (ct < 0.0) { + // we look down + distance_in_layer = dist; + vAltitude = min(distance_in_layer,min(visibility, avisibility)) * ct; + delta_zv = delta_z - vAltitude; + } else { + // we may look through upper layer edge + H = dist * ct; + if (H > delta_z) { + distance_in_layer = dist/H * delta_z; + } else { + distance_in_layer = dist; + } + vAltitude = min(distance_in_layer,visibility) * ct; + delta_zv = delta_z - vAltitude; + } + } else { + // we see the layer from above, delta_z < 0.0 + H = dist * -ct; + if (H < (-delta_z)) { + // we don't see into the layer at all, aloft visibility is the only fading + distance_in_layer = 0.0; + delta_zv = 0.0; + } else { + vAltitude = H + delta_z; + distance_in_layer = vAltitude/H * dist; + vAltitude = min(distance_in_layer,visibility) * (-ct); + delta_zv = vAltitude; + } + } + + // ground haze cannot be thinner than aloft visibility in the model, + // so we need to use aloft visibility otherwise + + transmission_arg = (dist-distance_in_layer)/avisibility; + if (visibility < avisibility) { + transmission_arg = transmission_arg + (distance_in_layer/visibility); + } else { + transmission_arg = transmission_arg + (distance_in_layer/avisibility); + } + + transmission = fog_func(transmission_arg); + float lightArg = terminator/100000.0; + float attenuationScale = 1.0 + 20.0 * (1.0 -smoothstep(-15.0, 0.0, lightArg)); + float dist_att = exp(-dist/200.0/lightSize/attenuationScale); + + float intensity = light_sprite(gl_TexCoord[0].st, transmission, noise); + vec3 light_color = gl_Color.rgb; + + if (use_night_vision) { + light_color.rgb = vec3 (0.0, 1.0, 0.0); + } + + light_color = mix(light_color, vec3 (1.0, 1.0, 1.0), 0.5 * intensity * intensity); + gl_FragColor = vec4 (clamp(light_color.rgb, 0.0, 1.0), intensity * transmission * dist_att); +} diff --git a/Shaders/scenery-lights.vert b/Shaders/scenery-lights.vert new file mode 100644 index 000000000..b31add3f0 --- /dev/null +++ b/Shaders/scenery-lights.vert @@ -0,0 +1,230 @@ +// -*-C++-*- +#version 120 + +// Shader that takes a list of GL_POINTS and draws a light (point-sprite like +// texture, more accurately a light halo) at the given point. This shader +// provides support for light animations like blinking, time period handling +// for lights on only during night time or in low visiblity and directional +// lighting. +// +// The actual rendering code is inspired from an existing implementation +// found at: +// FGData commit 9355d464c175bd5d51ba32527180ed4e94e86fbb +// Shaders/surface-lights-ALS.vert +// with major changes. +// +// Licence: GPL v2+ +// Written by Fahim Dalvi, January 2021 + +attribute vec3 lightParams; +attribute vec4 animationParams; +attribute vec3 directionParams1; +attribute vec2 directionParams2; +uniform float osg_SimulationTime; + +uniform float avisibility; +uniform float sun_angle; + +varying vec3 relativePosition; +varying vec2 rawPosition; + +varying float apparentSize; +varying float haloSize; +varying float lightSize; +varying float lightIntensity; + +const float epsilon = 1e-7; + +// rand2D sourced from noise.frag, since *.vert files +// cannot access functions defined in *.frag files +// Git commit: b8ddf517f4495219da7675d81bed59a378e2d78a +// File: fgdata/Shaders/noise.frag +float rand2D(in vec2 co){ + return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); +} + +void main() +{ + /***************************** Initialization ****************************/ + float random = rand2D(vec2(gl_Vertex.x, gl_Vertex.yz)) * 10; + float random_1 = floor(random); // random_1 can take 10 values + float random_2 = fract(random); // random_2 takes the remaining random bits + + /*************** Night and low visibility lights handling ****************/ + int on_period = int(lightParams.z + 0.5); // round() not supported by glsl 1.2 + const float sun_angle_min = 1.57; + const float sun_angle_max = 1.61; + float target_sun_angle = sun_angle_min + random_1/10 * (sun_angle_max - sun_angle_min); + if (on_period == 1 && sun_angle < sun_angle_min) { + // Lights will switch on exactly at ~89 degree sun angle + gl_Position = vec4(0.0,0.0,10.0,1.0); + gl_FrontColor.a = 0.0; + return; + } else if (on_period == 2 && sun_angle < target_sun_angle) { + // Lights will switch on randomly between 90 and 92 degree sun angle + // corresponding to a ~10 minute period around sunset + gl_Position = vec4(0.0,0.0,10.0,1.0); + gl_FrontColor.a = 0.0; + return; + } else if (on_period == 3 && (sun_angle < sun_angle_min && avisibility > 5000)) { + // Lights will switch on exactly at ~89 degree sun angle or when visibility + // is less than 5000m + gl_Position = vec4(0.0,0.0,10.0,1.0); + gl_FrontColor.a = 0.0; + return; + } + + /****************************** Animations *******************************/ + float interval = animationParams.x; + if (interval > 0) { + float on_portion = animationParams.y; + float strobe_rate = animationParams.z; + float offset = animationParams.w; + + // Randomize offset if its less than 0 + if (offset < 0) { + // rand2D returns a value from 0 to 1, multiplying it with + // the interval chooses an offset within the entire animation + // window + offset = random_2 * interval; + } + + float strobe_interval = interval/strobe_rate; + float interval_fraction = mod(osg_SimulationTime + offset, interval)/interval; + float strobe_fraction = mod(osg_SimulationTime + offset, strobe_interval)/strobe_interval; + + if (interval_fraction > on_portion || (strobe_fraction < 0.5 && strobe_rate > 0.0000001)) { + gl_Position = vec4(0.0,0.0,10.0,1.0); + gl_FrontColor.a = 0.0; + return; + } + } + + /***************************** Light visuals *****************************/ + gl_FrontColor = gl_Color; + gl_Position = ftransform(); + + vec4 eyePosition = gl_ModelViewMatrixInverse * vec4(0.0,0.0,0.0,1.0); + relativePosition = gl_Vertex.xyz - eyePosition.xyz; + rawPosition = gl_Vertex.xy; + float dist = length(relativePosition); + float angularAttenuationFactor = 1.0; + + /************************** Direction handling ***************************/ + if (directionParams2.x < 359.999999 || directionParams2.y < 359.999999) { + vec3 eyeVector = normalize(-relativePosition); + vec3 lightNormal = normalize(directionParams1); + vec3 upVec = normalize(vec3(0,0,1)); + + vec3 horizontalVector, verticalVector; + + if (abs(dot(lightNormal, upVec)) > (1 - epsilon)) { + // Light direction is directly up or down + horizontalVector = normalize(vec3(1, 0, 0)); + verticalVector = normalize(vec3(0, 1, 0)); + } else { + horizontalVector = normalize(cross(lightNormal, upVec)); + verticalVector = normalize(cross(lightNormal, horizontalVector)); + } + + vec3 projectionOnHorizontal = lightNormal; + vec3 projectionOnVertical = lightNormal; + + if (dot(lightNormal, eyeVector) < (-1 + epsilon)) { + // If the view direction is directly opposite to the light normal + projectionOnHorizontal = eyeVector; + projectionOnVertical = eyeVector; + } else { + // If the view vector is not perpendicular to the horizontal axis + if (abs(dot(horizontalVector, eyeVector)) > (0 + epsilon)) { + projectionOnHorizontal = normalize(eyeVector - dot(verticalVector, eyeVector) * verticalVector); + } + + // If the view vector is not perpendicular to the vertical axis + if (abs(dot(verticalVector, eyeVector)) > (0 + epsilon)) { + projectionOnVertical = normalize(eyeVector - dot(horizontalVector, eyeVector) * horizontalVector); + } + } + + float horizontalAngle = dot(projectionOnHorizontal, lightNormal); + float verticalAngle = dot(projectionOnVertical, lightNormal); + + float minHoriz = cos(radians(directionParams2.x * 0.5)); + float minVert = cos(radians(directionParams2.y * 0.5)); + + // Light is 0 intensity below [specified angle] + // Increases softmax-ly between [specified angle] and [1/2 of difference of specified angle and 0] (head on viewing) + // Light is 1 intensity after [1/2 of difference of specified angle and 0] to [0 degrees] + // Note: difference of angles is computed linearly after applying the cosine function, but it works well enough as an approximation + horizontalAngle = smoothstep(minHoriz, minHoriz + (1 - minHoriz)/2.0, horizontalAngle); + verticalAngle = smoothstep(minVert, minVert + (1 - minVert)/2.0, verticalAngle); + angularAttenuationFactor = horizontalAngle*verticalAngle; + + // Debug animation code + // float ra = mod(osg_SimulationTime*30, 20); + // gl_FrontColor = vec4(verticalAngle, 0, 0, 1); + // if (ra < 10) { + // gl_Position = gl_ModelViewProjectionMatrix * (gl_Vertex + (ra)/2 * normalize(vec4(directionParams1, 0))); + // gl_FrontColor = vec4(1.0, 0.0, 0.0, 1.0); + // } else if (ra < 20) { + // // gl_Position = gl_ModelViewProjectionMatrix * (gl_Vertex + (ra-15) * normalize(vec4(proj_on_horizontal, 0))); + // // gl_FrontColor = vec4(0.0, 0.0, 1.0, 1.0); + // } else if (ra < 30) { + // // gl_Position = gl_ModelViewProjectionMatrix * (gl_Vertex + (ra-25) * normalize(vec4(vertical_vec, 0))); + // gl_FrontColor = vec4(0.0, 1.0, 0.0, 1.0); + // } else if (ra < 40) { + // gl_Position = gl_ModelViewProjectionMatrix * (gl_Vertex + (ra-35) * normalize(vec4(horizontal_vec, 0))); + // gl_FrontColor = vec4(0.0, 1.0, 1.0, 1.0); + // } else { + // gl_Position = gl_ModelViewProjectionMatrix * (gl_Vertex + (ra-45) * normalize(vec4(proj_on_vertical, 0))); + // gl_FrontColor = vec4(0.0, 0.0, 1.0, 1.0); + // } + } + + lightSize = lightParams.x; + lightIntensity = lightParams.y; + + /******* + * TODOs: + * Might need to take into account FOV + */ + + /******** + * Each light is made up of a base circle, a circular-ish halo around the base and a bunch of + * star-like rays + * baseLightSize is tuned using reference objects of sizes 10cm, 50cm, 100cm, 500cm and 1000cm + * under the assumption that the "bright center" part of the light will be the same size as + * the light itself + */ + float baseLightSize = lightSize / (dist/80); + + + /******** + * Decide how big the halo + star like structure can get relative to the actual light size + * This has been done by fitting various curve (using random parameter search to fit the + * following data): + + dist/intensity -> haloSize + 0/2070 -> 2 + 0/15000 -> 2 + 1900/2070 -> 10 + 33250/15000 -> 30 + + * The real world distance to dist mapping is around the following: + 1nm = 1930 + 2nm = 3780 + 3nm = 5630 + 4nm = 7480 + 5nm = 9330 + */ + + // Various fits that are better at different "zones" of intensity/distance combinations + // haloSize = 1 + log(1 + 8.207628166987313e-05 + pow(0.00935009645108105 * dist, 0.7229519420159332)) * log(1 + 0.008611494896181404 + pow(9.987873482714503e-08 * lightIntensity, 0.5460367551326879)) * 188.78730222257022; + // haloSize = 1 + log(1 + pow(0.00344640493737296 * dist, 43.666719413543746)) * log(1 + pow(6.174965900415324e-07 * lightIntensity, 0.1915282228627938)) * log(1 + pow(48.81816078788492 * dist * lightIntensity, 0.39087987152530046)) * 0.03982200390091456; + // haloSize = 1 + 1 + log(1 + pow(0.003022850828231838 * dist, 81.88510919372303)) * log(1 + pow(2.8538041872684384e-05 * lightIntensity, 0.2798979878622515)) * log(1 + pow(6.125105317094489 * dist * lightIntensity, 9.486990540357818e-06)) * 0.17726981739920522; + // haloSize = 1 + (log(1 + 0.0009319617220954881 * dist) * log(1 + 0.3853503865089568 * lightIntensity)) * 0.8677850896527736; + haloSize = 1 + (log(1 + 0.0030356535475020265 * dist) * log(1 + 0.00964994652970935 * lightIntensity)) * 1.1927528593388748; + apparentSize = baseLightSize * haloSize * angularAttenuationFactor; + + gl_PointSize = apparentSize; +}