├── package.json ├── LICENSE ├── index.html ├── README.md └── super-sky.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-super-sky", 3 | "version": "1.1.1", 4 | "description": "drop-in full-featured sky", 5 | "main": "super-sky.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/kylebakerio/a-super-sky.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/kylebakerio/a-super-sky/issues" 18 | }, 19 | "homepage": "https://github.com/kylebakerio/a-super-sky#readme" 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kyle Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://data.jsdelivr.com/v1/package/npm/a-super-sky/badge)](https://www.jsdelivr.com/package/npm/a-super-sky) 2 | # a-super-sky 3 | 4 | Fancy, lightweight, drop-in day-night sky component for A-Frame. Confirmed working with A-Frame 1.4.1. 5 | 6 | Utilizes [a-sun-sky](https://supermedium.com/superframe/components/sun-sky/) and [aframe-star-system](https://github.com/handeyeco/aframe-star-system-component) for A-Frame <1.2.0 compatible stars; Also borrowed heavily from [aframe-environment-component](https://github.com/supermedium/aframe-environment-component/commit/ab99293ee54826923212aca0dfc112d35b64d970)'s "starry" preset, a static scene, that I used for the 1.2.0 stars and as a starting point to extrapolate fog color to light color. Beyond those, this library adds controls to enable and control animation, fog to create a more appealing (imo) star fade in/out effect, stronger sunset effect, and also adds a directional light entity that roughly tracks the sun/moon to allow shining a directional shadow-casting light source from the shader's "sun". Significant effort has been spent creating smooth transitions between all of these moving parts to create a coherent environment. 7 | 8 | Buy Me a Coffee at ko-fi.com 9 | 10 | ![moonlight-promo-19](https://user-images.githubusercontent.com/6391152/127781724-e853270a-4137-4f92-953d-2b18089b691e.png) 11 | ![sunset-room-shadow](https://user-images.githubusercontent.com/6391152/127769302-772c0c2d-246e-4c7e-87dd-f4a94e7b77b7.png) 12 | 13 | # features! 14 | - highly performant out of the box even with maximum settings enabled, and **auto-throttles self** if sky is slow enough to allow. 15 | - sunrise, daytime, and sunset feature beautiful rayleigh scattering colors in the sky 16 | - `fog` creates feeling of creeping shadowy darkness after sunset, that then retreats as stars slowly fade into view and light the scene, without the cost of the equivalent shadows 17 | - subtle 'colored' fog and lighting on the horizon adds a sense of depth and realism to the day and night's different stages 18 | - moon also rises and sets, creating a blue rayleigh glow in the sky 19 | - at dawn, stars gently fade out, sky stars to go from soft blues to pre-sawn reds, and then fog again comes in just to create a feeling of shadows retreating as sun rises 20 | - **now with real time shadow-casting lighting from the sun and moon!** 21 | - intensity-matching hemisphere light for a natural/appropriate ambient lighting to match the directional lighting 22 | - all light sources can be enabled/disabled/adjusted, and have sane, natural feeling defaults (e.g., softer light from stars, soft light from moon) 23 | - pause, play, speed up, slow down all possible 24 | - calculate sky to align with any fixed epoch time, thereby making multi-user sync'd skies simple, as well as in-world consistency over many sessions 25 | 26 | # demos 27 | - play with live functioning code [on glitch](https://glitch.com/edit/#!/a-super-sky-demo?path=index.html%3A1%3A0) ([as a page](https://a-super-sky-demo.glitch.me/)) 28 | - this repo's demo html: index.html in this repo [live](https://kylebakerio.github.io/a-super-sky/) 29 | 30 | # compatibility 31 | 32 | ## System Resources + Performance 33 | - runs easily in oculus quest 2's native browser. this even runs smoothly in google cardboard, it seems. 34 | - to improve performance, set `orbitduration` as high as you are willing or want to, and then `throttle` as high as you can tolerate` before stuttering occurs. 35 | - you can remove light sources and remove or reduce the shadow-casting sun to further lighten the load. 36 | - you could also just use this statically instead of dynamically--when doing so, you can also pre-bake shadows for the environment, and only live calc shadows for e.g. your player's character, etc. 37 | 38 | ## A-Frame version 39 | - Tested working with 1.0.4, 1.1.0, 1.2.0, 1.3.0. 40 | 41 | # how to add 42 | 43 | add sources to project: 44 | 45 | ### >= 1.2.0 46 | add to sources: 47 | ```html 48 | 49 | ``` 50 | 51 | ### <= 1.1.0 (only tested as low as 1.0.4) 52 | add to the above: 53 | ```html 54 | 55 | ``` 56 | 57 | then add a super-sky entity to your scene: 58 | (it's recommended that you set `sunbeamTarget` to the selector that matches your user's camera) 59 | ```html 60 | 64 | ``` 65 | if using the very popular [aframe environment component](https://github.com/supermedium/aframe-environment-component), make sure you tell it to not set a sky: 66 | 67 | ```html 68 | 69 | ``` 70 | 71 | #### minimum options you should set 72 | - `orbitduration` is how long 1 sun loop takes (in minutes). By default, a full day is twice this length. (if `mooncycle="false"`, it's 1x this length.) 73 | - _optional_: set `throttle` as high as you can before it starts looking choppy. By default, it'll target 40 updates per second (throttle=25), but will proportionately auto-tune down from that if `orbitduration` starts to be longer than `10` (because that's when you stop needing 40 calcs per second to still get enough frames per arc angle in the sky for it to look smooth). You can set `super-sky-debug="true"` to see logging info about the auto-tune throttle state, or run `document.querySelector('[super-sky]').components['super-sky'].throttle` in your console. Net result: the longer your day, the lighter the computational load, and it'll always look smooth. 74 | - `groundcolor` should be set manually, and is used to calculate more realistic light color. 75 | - see super-sky.js schema for other options. comments explain their use. 76 | - see glitch demo or repo index.html (referenced above in this readme) for live examples if unclear. 77 | 78 | #### setting a static scene 79 | - `targetfpd="0"` will mean the scene will freeze and not run any update calculations 80 | - `startpercent=".25"` means you want the scene to be at the 25% of a full day/night cycle, while `.99` would be just before sunrise. `0` (default) is sunrise itself. `49` would be 1% before moon rises, `.75` would be 'moonset'. (If `mooncycle=false`, it's percent of just the sun rotation, instead of sun+moon rotations.) 81 | 82 | if you want shadows, add the `shadow` component to entities that you want to cast shadows and receive shadows (allow shadows to be casted upon): 83 | ```html 84 | 85 | 86 | ``` 87 | 88 | ## how does the auto-tuning work? 89 | - Set `orbitduration` to whatever you want. The higher the number, the slower the sky. The slower the sky, the less often calculations need to be done to update it while still looking smooth. 90 | - By default will auto-throttles sky calculation frequency according to duration--sets a 40fps cap by default, but will drop below that if 33 frames per degree of motion are being attained, which happens if the `orbitduration` starts to be above `11` (unit is in minutes), which would be 20 minutes for a day/night cycle if `mooncycle="true"`. You can see logged output regarding the tuning, throttle, fps, fpd (frames per degree), etc. when `super-sky-debug="true"`/. 91 | - You can manually set the throttle if desired, instead, which sets the minimum milliseconds to wait between calculations. Alternatively, you can set `targetfpd` (target frames per degree) if desired. 92 | 93 | ## Updating values after init? 94 | I want to assume it's somehow my fault, but for some reason in my tests `oldData` is almost always coming in as an empty object, and in general I'm observing very strange behavior when trying to use the 'correct' `update()` functionality for A-Frame components? I'll file an issue here, but in the meantime I've just worked around the issue... 95 | 96 | ### update speed of orbit without lurching sky 97 | `document.querySelector('[super-sky]').components['super-sky'].updateOrbitDuration(.1)` 98 | ### pause sky suddenly 99 | `document.querySelector('[super-sky]').components['super-sky'].updateOrbitDuration(10000)` 100 | ### play again 101 | `document.querySelector('[super-sky]').components['super-sky'].updateOrbitDuration(.1)` 102 | ### sync user time? 103 | Easiest way? Pick hardcoded 'starttime', 'orbitduration' and 'mooncycle' values for your app. Voila, if their browser has correct time, then it'll always be in sync for all users. Use the correct/same values at instantiation. 104 | 105 | snap/sync after the fact can be done for now this way: 106 | ```js 107 | // comp1 108 | let user1Sky = JSON.stringify(document.querySelector('[super-sky]').components['super-sky'].shareSky()) 109 | // "{\"mooncycle\":true,\"orbitduration\":1.1,\"starttime\":1628045331326}" 110 | // comp2 111 | let user1Sky = JSON.parse( 112 | "{\"mooncycle\":true,\"orbitduration\":1.1,\"starttime\":1628045331326}" 113 | ); 114 | document.querySelector('[super-sky]').components['super-sky'].data.mooncycle = user1Sky.mooncycle 115 | document.querySelector('[super-sky]').components['super-sky'].updateOrbitDuration(user1Sky.orbitduration) 116 | document.querySelector('[super-sky]').components['super-sky'].data.starttime = user1Sky.starttime 117 | document.querySelector('[super-sky]').components['super-sky'].updateSkyEpoch() 118 | ``` 119 | 120 | # Tips & FAQ 121 | ## shadows & lighting 122 | #### My ground is too shiny / the reflections from the sun/moon are too intense 123 | Add 'roughness' to the material to reduce shininess. In my demos, for example, I use `material="roughness:.633"` on the `` that acts as the ground 124 | #### Shadows cut off when I am not close enough to them 125 | - Shadows aren't trivial and come with a cost. You can up the shadow render square with the `shadowsize` attribute, which underneath affects the `light.shadowCamera` left/right/top/bottom values. This is how many meters from the `sunbeamtarget` you calculate shadows from. Keep in mind that too large and the shadows get lower res... you'll need to start crafting some custom light/shadowCamera values here. 126 | - You might also change the `sunbeamtarget` attribute, depending on your scene. This is the center of the invisible box around which shadows are calculated. By default, in this app, it's the user's view (so, `[camera]`), and the default is the `shadowsize` attribute of 15m (so, shadows are calculated within a 30m box around the user). If you only have things shadows should be cast from in the middle of the map, but want to see them even from far away, setting the target to the center of your map would work. This will make surface reflections during sunrise/sunset be a bit off, though, so probably compensate for that (e.g., add roughness to the appropriate material) to cover that up. 127 | - You can consider obscuring the user's field of view so that they can rarely see out in the distance, as well, to prevent this from being an issue. (e.g., high features surround you, instead of wide open vistas). 128 | #### It gets too dark at night 129 | Add a light source if your scene can handle it, or adjust `starlightintensity` to a higher than default (.1) value (like .2) if you want to use fewer lights for higher performance/simplicity. 130 | #### I get weird shadow artifacts 131 | Look into [shadow bias](https://aframe.io/docs/1.2.0/components/light.html#configuring_shadows_shadowbias), and if you changed the default values for the `sunbeamdistance`, that's probably the cause. 132 | #### How do I turn off shadows? 133 | `showshadowlight='false'` 134 | #### How do I keep shadows but reduce their performance cost? 135 | - make `shadowsize` smaller (default is 15) 136 | - make `orbitduration` as high as you are willing to, and then make `throttle` (default 10ms) as high as you can before the updates look choppy. 137 | - change `shadowMapHeight` and `shadowMapWidth` of the `sunbeam` to a lower value (default is 1024; try 512, or even further down to 256). 138 | - remove other light sources, and/or reduce the number of items that `cast` and `receive` `shadow`. 139 | #### My question isn't here. 140 | You can file an issue, I'd be interested to hear. But honestly, you should probably see [the docs](https://aframe.io/docs/1.2.0/components/light.html#configuring-shadows) to dig deeper into this. I'm not an expert. 141 | ## other 142 | #### How can I make the fog not apply to X? 143 | [As per the docs](https://aframe.io/docs/1.2.0/components/fog.html), add `material="fog:false;"` to X. 144 | 145 | #### How can I know what "time of day" it is? 146 | ```js 147 | document.querySelector('[super-sky]').components['super-sky'].timeOfDay() 148 | ``` 149 | 150 | # TODO: 151 | #### enable update functionality 152 | - figure out why A-Frame's `update()` functionality seems completely borked? Have worked around it for now, but mostly updating values in a-frame inspector, for example, won't work. Perhaps related to setting `this.data` directly? 153 | - add in features from https://github.com/EX3D/aframe-daylight-system ! 154 | - some minor stuff gets out of sync if you leave the tab and come back, or change the cycle duration--specifically, for example, the 'timeOfDay' functionality. 155 | 156 | #### spinny stuff 157 | - better method for changing moon rise/set position than a-scene rotation? finishing implementing rotation option 158 | - slightly rotate stars over the course of a night 159 | - enable better control of sun/moon trajectory through sky (allow e.g. lower moon) 160 | 161 | #### time of day hooks 162 | - make it so that you can assign functions to be triggered at a given certain time of day. 163 | - add in default example for crickets audio playing at night 164 | - add in default example for shooting stars at night 165 | 166 | #### etc 167 | - currently night is 3x the length of day. This would imitate only northern winters/southern summers that have 8 hours of daylight, e.g. 10am to 6pm. ability to tweak this would be desirable--maybe speeding up or skipping phases when neither sun nor moon 168 | - environment component has ability to put sun on the shader anywhere in the sky (using 3x 0-360 inputs), but this code needs updating to allow that. Doing so would allow moon to not follow exact same path as sun, which would be nice. 169 | - finish implementing existing options, and add some more of them 170 | - try [exponential fog](https://aframe.io/docs/1.2.0/components/fog.html) instead of linear. 171 | -------------------------------------------------------------------------------- /super-sky.js: -------------------------------------------------------------------------------- 1 | const AFRAME = window.AFRAME 2 | const THREE = window.THREE 3 | 4 | AFRAME.registerPrimitive('a-super-sky', { 5 | defaultComponents: { 6 | // mimic the a-sky primitive, to paint our shader on the inside of a sphere as designed 7 | // https://github.com/aframevr/aframe/blob/master/src/extras/primitives/primitives/a-sky.js 8 | geometry: { 9 | primitive: 'sphere', 10 | // radius: 500, // see default 'sunshaderdistance' in component 11 | segmentsWidth: 64, 12 | segmentsHeight: 32 13 | }, 14 | material: { 15 | // color: '#FFF', 16 | // shader: 'flat', 17 | side: 'back', 18 | npot: true 19 | }, 20 | scale: '-1 1 1', 21 | 'super-sky': {}, 22 | }, 23 | mappings: { 24 | orbitduration: 'super-sky.orbitduration', 25 | throttle: 'super-sky.throttle', 26 | targetfpd: 'super-sky.targetfpd', 27 | starttime: 'super-sky.starttime', 28 | startpercent: 'super-sky.startpercent', 29 | mooncycle: 'super-sky.mooncycle', 30 | disablefog: 'super-sky.disablefog', 31 | fogmin: 'super-sky.fogmin', 32 | showstars: 'super-sky.showstars', 33 | starcount: 'super-sky.starcount', 34 | starlightintensity: 'super-sky.starlightintensity', 35 | starloopstart: 'super-sky.starloopstart', 36 | showhemilight: 'super-sky.showhemilight', 37 | showsurfacelight: 'super-sky.showsurfacelight', 38 | showshadowlight: 'super-sky.showshadowlight', 39 | groundcolor: 'super-sky.groundcolor', 40 | sunbeamdistance: 'super-sky.sunbeamdistance', 41 | starfielddistance: 'super-sky.starfielddistance', 42 | sunshaderdistance: 'super-sky.sunshaderdistance', 43 | sunbeamtarget: 'super-sky.sunbeamtarget', 44 | shadowsize: 'super-sky.shadowsize', 45 | seed: 'super-sky.seed', 46 | moonrayleigh: 'super-sky.moonrayleigh', 47 | moonluminance: 'super-sky.moonluminance', 48 | moonturbidity: 'super-sky.moonturbidity', 49 | moonintensity: 'super-sky.moonintensity', 50 | sunintensity: 'super-sky.sunintensity', 51 | sunrayleigh: 'super-sky.sunrayleigh', 52 | sunluminance: 'super-sky.sunluminance', 53 | sunturbidity: 'super-sky.sunturbidity', 54 | sunriseoffset: 'super-sky.sunriseoffset', 55 | moonriseoffset: 'super-sky.moonriseoffset', 56 | 'super-sky-debug': 'super-sky.debug', 57 | } 58 | }); 59 | 60 | 61 | 62 | AFRAME.registerComponent('super-sky', { 63 | schema: { 64 | orbitduration: { 65 | // time for one sun cycle. if moon cycle is enabled, then a full day will be twice this length. 66 | // set to `infinite` 67 | type: 'number', 68 | default: 1, // in minutes 69 | }, 70 | throttle: { // overrides targetfpd if set 71 | // -1 means that throttle will be auto-set according to targetfpd 72 | 73 | // how much to throttle, if desired 74 | // higher values make sky computed less often, which will make sky 'choppy' 75 | // to tune for performance: 76 | // make cycle duration as long as possible first, 77 | // then increase throttle as high as you can before it starts looking jerky 78 | type: 'number', 79 | default: -1, // min ms to wait before recalculating sky change since last calculation; e.g., 10 = 100fps cap 80 | }, 81 | targetfpd: { // fpd = frames per degree 82 | // -1 means that an auto-set value will be used 83 | // set to 0 to make scene static 84 | // fpd of '1' would mean the sun/moon will 'click' into each 1/360 position of its rotation, '2' would mean per .5 degree, and so on 85 | // sane value is usually around 10 (per 10th of a degree, attempt refresh) 86 | type: 'number', 87 | default: -1, 88 | }, 89 | starttime: { 90 | // this will be auto-set to date.now(), but you can specify a start time to sync the sky to other 91 | // users or to a consistent in-world epoch time. 92 | type: 'number', 93 | default: 0, 94 | }, 95 | startpercent: { 96 | // percent of full day/night (or day, if moon cycle disabled) to skip at initialization 97 | // .83 = start at 83% of a full cycle 98 | type: 'number', 99 | default: 0, 100 | }, 101 | 102 | mooncycle: { 103 | // turn on/off moon cycle; also turns off stars. nights are 'just black' when false. 104 | // relies on setting and controlling 'fog' component for a-scene. 105 | // night becomes 3x length, and features a 'moon' for 1/3 of that 106 | type: 'boolean', 107 | default: true, 108 | }, 109 | 110 | disablefog: { 111 | type: 'boolean', 112 | default: false, 113 | }, 114 | fogmin: { 115 | // how far you can see when night is at its darkest 116 | // which is, how close the shadows creep in to the user 117 | type: 'number', 118 | default: 70, // number from 0 -> 360 119 | }, 120 | 121 | showstars: { 122 | // use if you want to show moon, but no stars 123 | type: 'boolean', 124 | default: true, 125 | }, 126 | starcount: { 127 | // how many stars to show at night 128 | type: 'number', 129 | default: 2000, 130 | }, 131 | starlightintensity: { 132 | // used as minimum light intensity at any time of active lights, which is, 133 | // how bright ambient lighting is when only stars are out 134 | type: 'number', 135 | default: 0.2, //number from 0 to 1 136 | }, 137 | moonintensity: { 138 | // how bright the actual light from the moon is 139 | // this is just a multiplier used, actual value is dynamic according to position in sky 140 | type: 'number', 141 | default: 0.05, //number from 0 to 1 142 | }, 143 | sunintensity: { 144 | // how bright the actual light from the moon is 145 | // this is just a multiplier used, actual value is dynamic according to position in sky 146 | type: 'number', 147 | default: 0.5, //number from 0 to 1 148 | }, 149 | starloopstart: { 150 | // you shouldn't touch this. 151 | // sets offset (compare with no offset at 180) of when stars should rise and fog should creep in 152 | // code treats this as a constant, likely will break things if adjusted 153 | type: 'number', 154 | default: 200, // number from 0 -> 360; 180 is sunset 155 | // should have been 20, not 200. derp. that is where the forwarrd offset by ~2/8 comes from. 156 | }, 157 | 158 | showhemilight: { 159 | type: 'boolean', 160 | default: true, 161 | }, 162 | showsurfacelight: { 163 | type: 'boolean', 164 | default: false, 165 | }, 166 | showshadowlight: { 167 | type: 'boolean', 168 | default: true, 169 | }, 170 | groundcolor: { 171 | // gets mixed in for calculations about sunlight colors, fog color 172 | type: 'color', 173 | default: '#553e35', 174 | }, 175 | 176 | sunbeamdistance: { 177 | // how far away the shadow-casting sun is 178 | // while the shader for the sun/moon can be virtually any length, 179 | // if the sunbeam light source is too far away, shadows don't work properly 180 | // should always be at least a few meters smaller than the sunshaderdistance, 181 | // but no camera movement = any distance works, and less camera movement = more distance works 182 | type: 'number', 183 | default: 900, 184 | }, 185 | starfielddistance: { 186 | // how far away the stars are 187 | // correct distance is important to make sure fog shows/hides them with correct timing 188 | // should be within (so, smaller than) the sunshaderdistance 189 | // should also be within the sunbeam distance, or they won't be illuminated at night 190 | // they are finicky on being illuminated properly at diferent depths 191 | // starfield depth will be auto-set to be the difference between starfield and shader, 192 | type: 'number', 193 | default: 300, 194 | }, 195 | sunshaderdistance: { 196 | // the 'sun'/'moon' you see is projected on the inside of a sphere. this can be any distance by itself, 197 | // however, if too close, you'll 'run into' it (creating a 'truman show' effect), but the further the discrepancy 198 | // between this and sunbeam, the more 'off' the shadows and reflections will feel from the shader 199 | type: 'number', 200 | default: 1000, 201 | }, 202 | 203 | sunbeamtarget: { 204 | // target of directional sunlight that casts shadows 205 | // see: https://aframe.io/docs/1.2.0/components/light.html#directional 206 | // and: https://threejs.org/docs/#api/en/lights/directionallight 207 | type: 'string', 208 | default: "[camera]", 209 | }, 210 | shadowsize: { 211 | // size of shadow-casting beam of directional light 212 | // smaller is more performant, but cuts off shadow calculation within a tighter box around the target 213 | type: 'number', 214 | default: 15, // in meters 215 | }, 216 | 217 | seed: { 218 | // currently only used for star position in aframe >=1.2.0 219 | type: 'number', 220 | default: 1, 221 | }, 222 | 223 | // shader variations between night and day: 224 | // moonheight // todo 225 | moonrayleigh: { 226 | //number from 0 to 1; higher causes more red in the sky 227 | type: 'number', 228 | default: 0.1, 229 | }, 230 | moonluminance: { 231 | // recommended range: 1.09 to 1.19; 1.18 creates a beautiful halo; 1.19 creates a serene, simple moon; go higher, things get weird; go lower, skyline turns red 232 | type: 'number', 233 | default: 1.18, 234 | }, 235 | moonturbidity: { 236 | //number from 0 to 1 // moon aura size, especially at sunrise/sunset 237 | type: 'number', 238 | default: .3, 239 | }, 240 | sunrayleigh: { 241 | type: 'number', 242 | default: 1, //number from 0 to 1 243 | }, 244 | sunluminance: { 245 | type: 'number', 246 | default: 1, //number from 0 to 1 247 | }, 248 | sunturbidity: { 249 | type: 'number', 250 | default: 0.8, //number from 0 to 1 251 | }, 252 | 253 | sunriseoffset: { 254 | // experimental, not recommended, problems if anything has absolute positioning 255 | // where on the horizon the sun rises from 256 | // accomplishes this by rotating a-scene 257 | type: 'number', 258 | default: 0, // number from 0 -> 360 259 | }, 260 | moonriseoffset: { 261 | // experimental, not recommended, problems if anything has absolute positioning 262 | // where on the horizon the moon rises from 263 | // this is accomplished adding rotation to a-scene 264 | type: 'number', 265 | default: 45, // number from 0 -> 360 266 | }, 267 | 268 | debug: { 269 | type: 'boolean', 270 | default: false, 271 | }, 272 | }, 273 | 274 | init: function () { 275 | // see update() 276 | this.el.classList.add('super-sky'); 277 | this.tickBackup = this.tick; 278 | 279 | if (AFRAME.version === "1.0.4" || AFRAME.version === "1.1.0" || AFRAME.version[0] === "0") { 280 | if (this.data.debug) console.warn("detected pre 1.2.0, make sure to include star library (see readme)"); 281 | this.version = 0; 282 | } 283 | else { 284 | if (this.data.debug) console.log("detected AFRAME >=1.2.0, highest tested is 1.2.0"); 285 | this.version = 1; 286 | } 287 | 288 | this.sky = this.el; 289 | this.sky.classList.add('star-sky'); 290 | this.sky.setAttribute('material', 'opacity', 0); // may not be necessary anymore 291 | this.sky.setAttribute('material', 'shader', 'sky'); 292 | this.sky.setAttribute('geometry', 'thetaLength', 110); 293 | this.fogRangeMax = this.data.starfielddistance * 2; 294 | }, 295 | 296 | throttle: 0, // dynamically set 297 | // initializer functions, broken out from this.init() so that they can be re-called from update() if needed 298 | f: { 299 | setThrottle() { 300 | this.msPerDegree = (this.data.orbitduration/360)*1000*60; 301 | 302 | if (this.data.targetfpd === -1) { 303 | if (this.data.debug) console.log("no given targetfpd, will fall back to 40fps (throttle=25) / 60fpd cap default"); 304 | this.data.targetfpd = this.msPerDegree / 25 // first aim for 40fps minimum, but... 305 | this.data.targetfpd = this.data.targetfpd > 60 ? 60 : this.data.targetfpd; // if we're getting more than 60 renders per degree, set the ceiling her to massively reduce renders 306 | } 307 | 308 | if (this.data.throttle !== -1) { 309 | if (this.data.debug) console.warn("custom throttle set, will ignore targetfpd") 310 | this.throttle = this.data.throttle; 311 | this.data.targetfpd = this.msPerDegree / this.throttle; 312 | } 313 | else if (this.data.targetfpd === 0) { 314 | if (this.data.debug) console.warn("static sky") 315 | this.tickBackup = this.tick; 316 | this.throttle = Infinity; 317 | this.data.throttle = Infinity; 318 | return 319 | } 320 | else { 321 | this.throttle = this.msPerDegree / this.data.targetfpd; 322 | } 323 | 324 | this.fps = 1000/this.throttle; 325 | 326 | if (this.data.debug) { 327 | console.log(this.cleanNumber(this.msPerDegree),'ms per degree at',this.cleanNumber(this.data.targetfpd),',fpd; throttle:',this.cleanNumber(this.throttle),', fps:',this.cleanNumber(this.fps)); 328 | if (this.throttle < 10 || this.fps > 60) console.warn("likely wasteful rendering; consider setting `targetfpd` below", this.cleanNumber(this.data.targetfpd / (15 / this.throttle)),"to optimize performance"); 329 | else if (this.data.targetfpd < 60 && this.fps < 40) console.warn("animation may be choppy; if needed, consider increasing cycleduration, or setting `targetfpd` above", this.cleanNumber(this.data.targetfpd / (50 / this.throttle))+0.01,"to improve"); 330 | } 331 | 332 | this.tick = AFRAME.utils.throttleTick(this.tickBackup, this.throttle, this); 333 | }, 334 | startFromPercent() { 335 | if (this.data.startpercent < 0) this.data.startpercent = 1 - (-this.data.startpercent % 1) ; 336 | if (this.data.mooncycle && (this.data.startpercent % 1) >= .5) { 337 | this.secondHalf = true; 338 | this.moon = true; 339 | } 340 | this.moonSunSwitchFlag = false; 341 | const cycleInMs = (this.data.orbitduration * 1000 * 60) * (this.data.mooncycle ? 2 : 1) 342 | this.data.starttime = Date.now() - (this.data.startpercent * cycleInMs); 343 | this.getOrbit() 344 | this.setStarLoop() 345 | // add stars if needed 346 | this.getEighth() 347 | this.getEighthStarLoop() 348 | 349 | if (this.inRange(this.ranges.extendedStarRange)) { 350 | if (this.data.debug) console.warn("add stars to init") 351 | this.setStars() 352 | } else { 353 | if (this.data.debug) console.warn("remove stars from init") 354 | this.setStars(0) 355 | } 356 | 357 | if (this.data.debug) { 358 | console.log("skipping first",`${this.data.startpercent*100}%`,(this.data.startpercent * cycleInMs)/1000,'seconds of orbitduration',cycleInMs/1000,Date.now(),'-',this.data.starttime) 359 | console.log(this.orbit,this.currentEighth,this.currentEighthStarLoop) 360 | } 361 | }, 362 | hemilight(){ 363 | this.hemilight = document.createElement('a-entity'); 364 | this.hemilight.setAttribute('id','hemilight'); 365 | this.hemilight.setAttribute('position', '0 50 0'); 366 | this.hemilight.setAttribute('light', { 367 | type: 'hemisphere', 368 | color: '#CEE4F0', 369 | intensity: 0.1 370 | }); 371 | this.hemilight.setAttribute('visible', true); 372 | 373 | if (this.hemilight) this.hemilight.setAttribute('light', {groundColor: this.data.groundcolor}); 374 | this.el.appendChild(this.hemilight); 375 | this.activeLights.push(this.hemilight) 376 | }, 377 | sunbeam() { 378 | this.sunbeam = document.createElement(this.data.debug ? 'a-sphere' : 'a-entity') 379 | this.sunbeam.setAttribute('light', { 380 | type: 'directional', 381 | intensity: 0.1, // dynamically set during the day 382 | groundColor: this.data.groundcolor, 383 | 384 | shadowCameraVisible: this.data.debug, 385 | castShadow: true, 386 | 387 | shadowMapWidth: 1024, 388 | shadowMapHeight: 1024, 389 | shadowRadius: 1, 390 | 391 | shadowCameraLeft: -this.data.shadowsize, 392 | shadowCameraBottom: -this.data.shadowsize, 393 | shadowCameraRight: this.data.shadowsize, 394 | shadowCameraTop: this.data.shadowsize, 395 | 396 | shadowCameraFar: this.data.sunbeamdistance + 100, // this.data.shadowsize, // shadowsize was not enough 397 | shadowCameraNear: this.data.sunbeamdistance - 100, // could probably safelu lower this substantially... 398 | 399 | target: this.data.sunbeamtarget || "[camera]", 400 | }); 401 | this.sunbeam.setAttribute('id', 'sunbeam') 402 | // this.sunbeam.setAttribute('scale', '1 1 1') 403 | this.el.appendChild(this.sunbeam); 404 | this.activeLights.push(this.sunbeam) 405 | }, 406 | surfaceLight() { 407 | if (this.data.debug) { 408 | this.sunlight = document.createElement('a-sphere'); 409 | const nose = document.createElement('a-cone'); 410 | nose.setAttribute('position','z',1.15) 411 | nose.setAttribute('rotation',{'x':270, z:180}) 412 | nose.setAttribute('scale',{x:.2,y:.2,z:.2}) 413 | nose.setAttribute('material', 'color', 'blue') 414 | this.sunlight.appendChild(nose) 415 | } else { 416 | this.sunlight = document.createElement('a-entity'); 417 | } 418 | 419 | this.sunlight.setAttribute('id','sunlight-noshadow'); 420 | this.sunlight.setAttribute('light', { 421 | type:'directional', 422 | intensity: 0.1, 423 | shadowCameraVisible: this.data.debug, 424 | castShadow: false, 425 | shadowCameraLeft: -this.data.shadowsize, 426 | shadowCameraBottom: -this.data.shadowsize, 427 | shadowCameraRight: this.data.shadowsize, 428 | shadowCameraTop: this.data.shadowsize 429 | }); 430 | this.sunlight.setAttribute('visible', true); 431 | this.el.appendChild(this.sunlight); 432 | this.activeLights.push(this.sunlight) 433 | }, 434 | }, 435 | 436 | activeLights: [], 437 | 438 | // { 439 | // orbitduration: 'super-sky.orbitduration', 440 | // throttle: 'super-sky.throttle', 441 | // targetfpd: 'super-sky.targetfpd', 442 | // starttime: 'super-sky.starttime', 443 | // startpercent: 'super-sky.startpercent', 444 | // mooncycle: 'super-sky.mooncycle', 445 | // disablefog: 'super-sky.disablefog', 446 | // fogmin: 'super-sky.fogmin', 447 | // showstars: 'super-sky.showstars', 448 | // starcount: 'super-sky.starcount', 449 | // starlightintensity: 'super-sky.starlightintensity', 450 | // starloopstart: 'super-sky.starloopstart', 451 | // showhemilight: 'super-sky.showhemilight', 452 | // showsurfacelight: 'super-sky.showsurfacelight', 453 | // showshadowlight: 'super-sky.showshadowlight', 454 | // groundcolor: 'super-sky.groundcolor', 455 | // sunbeamdistance: 'super-sky.sunbeamdistance', 456 | // starfielddistance: 'super-sky.starfielddistance', 457 | // sunshaderdistance: 'super-sky.sunshaderdistance', 458 | // sunbeamtarget: 'super-sky.sunbeamtarget', 459 | // shadowsize: 'super-sky.shadowsize', 460 | // seed: 'super-sky.seed', 461 | // moonrayleigh: 'super-sky.moonrayleigh', 462 | // moonluminance: 'super-sky.moonluminance', 463 | // moonturbidity: 'super-sky.moonturbidity', 464 | // moonintensity: 'super-sky.moonintensity', 465 | // sunintensity: 'super-sky.sunintensity', 466 | // sunrayleigh: 'super-sky.sunrayleigh', 467 | // sunluminance: 'super-sky.sunluminance', 468 | // sunturbidity: 'super-sky.sunturbidity', 469 | // sunriseoffset: 'super-sky.sunriseoffset', 470 | // moonriseoffset: 'super-sky.moonriseoffset', 471 | // 'super-sky-debug': 'super-sky.debug', 472 | // } 473 | changed(key) { 474 | return this.oldData[key] !== this.data[key] 475 | }, 476 | isEmptyObject: function(testObj) { 477 | return (Object.keys(testObj).length === 0 && testObj.constructor === Object); 478 | }, 479 | update(oldData) { 480 | // I think this should be handled differently, and is probably automatically handled by tick() behavior 481 | // if (this.data.groundcolor != oldData.groundcolor) { 482 | // this.activeLights.forEach(el => { 483 | // el.setAttribute('light', {'groundColor': this.data.groundcolor}); 484 | // }); 485 | // } 486 | 487 | if (this.data.debug) console.log('update dif', AFRAME.utils.diff (oldData, this.data)) 488 | 489 | this.oldData = oldData; 490 | let firstUpdate = false; 491 | if (!this.firstUpdatePast) { 492 | this.firstUpdatePast = true; 493 | firstUpdate = true; 494 | // continue 495 | } else if (this.isEmptyObject(oldData)) { 496 | // attempts to prevent firing update whenever inspector is opened 497 | if (this.data.debug) console.warn("skipping update because of empty object... update() is broken") 498 | return 499 | } 500 | 501 | if (!this.data.debug) { 502 | this.trackPerformanceBackup = this.trackPerformance; 503 | this.trackPerformance = () => {}; 504 | } else if (this.trackPerformanceBackup) { 505 | this.trackPerformance = this.trackPerformanceBackup; 506 | } 507 | 508 | if (this.changed('throttle') || this.changed('targetfpd')) { 509 | if (this.data.debug) console.warn("throttle/targetfpd update") 510 | if (oldData.targetfpd === 0 && this.data.targetfpd !== 0) this.tick = this.tickBackup; // restore tick to attempt to allow restarting from static scene 511 | this.f.setThrottle.bind(this)() 512 | } 513 | 514 | if (this.changed('mooncycle') && !this.data.mooncycle) { 515 | // turning off moon cycle 516 | this.data.startpercent = this.orbit / 360; 517 | this.f.startFromPercent.bind(this)(); 518 | this.tickHandlers() // flip one frame forward to show the change while paused from inspector 519 | // this.tickBackup(); 520 | } 521 | 522 | 523 | if (this.changed('starttime') && this.data.starttime) { 524 | if (this.data.debug) console.log('will use custom starttime:', !!this.data.starttime) 525 | // do we even need to do anything? We could prevent 'jerk' by pausing until the times match, perhaps? Or just slow down by half until times match...? 526 | // probably 80/20 is to just ramp up/down fog 527 | // and run this.init? 528 | 529 | // new idea, we now have implemented .updateOrbitDuration(10) 530 | // so just do double their orbitduration until percent, and then back to same speed. 531 | // increased fog might also be nice, though. 532 | this.updateSkyEpoch() 533 | } 534 | else if (this.changed('startpercent') && this.data.startpercent) { 535 | if (this.data.debug) console.log('will use custom startpercent:', this.data.starttime) 536 | 537 | this.f.startFromPercent.bind(this)(); 538 | } 539 | else if (!this.data.starttime) { 540 | if (this.data.debug) console.log('no custom starttime:', this.data.starttime) 541 | this.data.starttime = Date.now(); 542 | } 543 | 544 | if (this.changed('showhemilight')) { 545 | if (this.data.debug) console.warn("hemi light change update") 546 | if (this.hemilight) this.el.removeChild(this.hemilight) 547 | if (this.data.showhemilight) this.f.hemilight.bind(this)(); 548 | } 549 | if (this.changed('showsurfacelight')) { 550 | if (this.data.debug) console.warn("surface light change update") 551 | if (this.sunlight) this.el.removeChild(this.sunlight) 552 | if (this.data.showsurfacelight) this.f.surfaceLight.bind(this)(); 553 | } 554 | if (this.changed('showshadowlight') || this.changed('shadowsize')) { 555 | if (this.data.debug) console.warn("sunbeam light change update") 556 | if (this.sunbeam) this.el.removeChild(this.sunbeam) 557 | if (this.data.showshadowlight) this.f.sunbeam.bind(this)(); 558 | } 559 | if (this.changed('sunshaderdistance')) { 560 | if (this.data.debug) console.warn("sunshader update") 561 | this.sky.setAttribute('geometry', 'radius', this.data.sunshaderdistance); 562 | } 563 | if (this.changed('sunbeamtarget')) { 564 | if (this.data.debug) console.warn("sunbeamtarget update") 565 | this.sunbeam?.setAttribute('light', 'target', this.data.sunbeamtarget) 566 | } 567 | 568 | if (firstUpdate) return 569 | // else 570 | // put things that should run on first update below this point 571 | 572 | if (this.changed('orbitduration')) { 573 | // for some reason, update() seems very broken, and this condition will only reliably fire the first time. needs to be done manually in another function, it seems. 574 | this.updateOrbitDuration(); 575 | } 576 | if (this.changed('starfielddistance') || this.changed('seed')) { 577 | if (this.data.debug) console.warn("starfield update") 578 | if (this.stars) { 579 | this.el.removeChild(this.stars); 580 | delete this.stars; 581 | } 582 | // no need to add them in, when `this.setStars()` is called, it'll create correctly as needed 583 | } 584 | 585 | if ( 586 | this.changed('moonrayleigh') || 587 | this.changed('moonluminance') || 588 | this.changed('moonturbidity') || 589 | this.changed('sunrayleigh') || 590 | this.changed('sunluminance') || 591 | this.changed('sunturbidity') 592 | ) { 593 | if (this.data.debug) console.warn("shader update") 594 | this.sunShaderTick() 595 | } 596 | 597 | if ( 598 | this.changed('groundcolor') || 599 | this.changed('moonintensity') || 600 | this.changed('sunintensity') || 601 | this.changed('starlightintensity') || 602 | this.changed('debug') //|| // debug, because we change some stuff for e.g. showing shadowcamera 603 | // this.changed('sunturbidity') 604 | ) { 605 | if (this.data.debug) console.warn("light color/brightness update") 606 | this.lightSourcesTick() 607 | } 608 | 609 | }, 610 | 611 | updateOrbitDuration(newOrbitDuration) { 612 | if (newOrbitDuration) { 613 | this.data.orbitduration = newOrbitDuration; 614 | } 615 | if (this.data.debug) console.warn("experimental: orbit duration shift without jerk?") 616 | const denom = this.data.mooncycle ? 8 : 4; 617 | this.data.startpercent = ((this.currentEighth.which+1) / denom) - ((1-this.currentEighth.percent)/denom) 618 | if (this.data.debug) console.log('current percent through cycle:',this.data.startpercent) 619 | // this.f.startFromPercent.bind(this)(); 620 | // this.tickHandlers() // flip one frame forward to show the change while paused from inspector 621 | if (this.data.debug) console.warn('will clear old targetfpd and auto-calc new value') 622 | this.data.targetfpd = -1 623 | this.data.throttle = -1 624 | this.f.setThrottle.bind(this)() 625 | this.f.startFromPercent.bind(this)(); 626 | }, 627 | 628 | dayPercent() { 629 | return (((this.currentEighth.which/8)+(1/8* this.currentEighth.percent)) + (.125*3) + this.data.startpercent) % 1; 630 | }, 631 | 632 | timeOfDay() { 633 | return `${this.padTime(Math.floor(this.dayPercent() * 24))}:${this.padTime(Math.floor(this.dayPercent() * 60 * 24) % 60)}:${this.padTime(Math.floor(this.dayPercent()* 60 * 60 * 24) % 60)}` 634 | }, 635 | 636 | padTime(n) { 637 | return (n+"").length < 2 ? "0" + n : ""+n; 638 | }, 639 | 640 | shareSky() { 641 | return { 642 | mooncycle: this.data.mooncycle, 643 | orbitduration: this.data.orbitduration, 644 | starttime: this.data.starttime, 645 | }; 646 | }, 647 | updateSkyEpoch(newStartTime=this.data.starttime){ 648 | // set moon cycle 649 | // set orbit duration 650 | // set starttime 651 | // call this function 652 | // e.g., 653 | /* 654 | screen1: 655 | let user1Sky = JSON.stringify(document.querySelector('[super-sky]').components['super-sky'].shareSky()) 656 | 657 | screen2: 658 | user1Sky = JSON.parse( 659 | user1Sky 660 | ) 661 | document.querySelector('[super-sky]').components['super-sky'].data.mooncycle = user1Sky.mooncycle 662 | document.querySelector('[super-sky]').components['super-sky'].updateOrbitDuration(user1Sky.orbitduration) 663 | document.querySelector('[super-sky]').components['super-sky'].data.starttime = user1Sky.starttime 664 | document.querySelector('[super-sky]').components['super-sky'].updateSkyEpoch() 665 | */ 666 | this.data.startpercent = this.timestampToOrbitPercent(newStartTime, this.data.mooncycle); 667 | if (this.data.debug) console.warn("will attempt to start with new time percent", this.data.startpercent) 668 | this.f.startFromPercent.bind(this)(); 669 | }, 670 | 671 | 672 | // Custom Math.random() with seed. Given this.environmentData.seed and x, it always returns the same "random" number 673 | random: function (x) { 674 | return parseFloat('0.' + Math.sin(this.data.seed * 9999 * x).toString().substr(7)); 675 | }, 676 | 677 | createStars: function() { 678 | this.stars = document.createElement('a-entity'); 679 | this.stars.setAttribute('id','stars'); 680 | 681 | if (this.data.showstars && this.version === 0) { 682 | if (this.data.debug) console.log("appending old style star system") 683 | // this.starsOld = document.createElement('a-entity'); 684 | // this.starsOld.setAttribute('id', 'stars'); 685 | 686 | // this.getSunSky().setAttribute('material', 'shader', 'sunSky'); 687 | // this.getSunSky().setAttribute('material','shader','sunSky'); 688 | // this.getSunSky().setAttribute('geometry','primitive','sphere'); 689 | // this.getSunSky().setAttribute('geometry','radius','1000'); 690 | 691 | this.stars.setAttribute('star-system',{ 692 | count: this.data.starcount, 693 | radius: this.data.starfielddistance, 694 | starSize: this.data.starfielddistance / 400, // .25 695 | depth: 25, // this.data.sunshaderdistance-this.data.starfielddistance, 696 | }); 697 | // this.el.appendChild(this.starsOld); 698 | } 699 | else { 700 | if (this.data.debug) console.log("appending new style star geometry") 701 | // initializes the BufferGeometry for the stars in >= AF 1.2.0 702 | // code here is more or less pulled from aframe-environment-component 703 | var numStars = this.data.starcount; 704 | var geometry = new THREE.BufferGeometry(); 705 | var positions = new Float32Array( numStars * 3 ); 706 | var radius = this.data.starfielddistance; 707 | var v = new THREE.Vector3(); 708 | for (var i = 0; i < positions.length; i += 3) { 709 | v.set(this.random(i + 23) - 0.5, this.random(i + 24), this.random(i + 25) - 0.5); 710 | v.normalize(); 711 | v.multiplyScalar(radius); 712 | positions[i ] = v.x; 713 | positions[i+1] = v.y; 714 | positions[i+2] = v.z; 715 | } 716 | geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 717 | geometry.setDrawRange(0, 0); // don't draw any yet 718 | var material = new THREE.PointsMaterial({size: 0.01, color: 0xCCCCCC, fog: false}); 719 | this.stars.setObject3D('mesh', new THREE.Points(geometry, material)); 720 | } 721 | this.el.appendChild(this.stars); 722 | }, 723 | 724 | setStars: function (starCount = this.howManyStars()) { 725 | this.starlight = !!starCount; 726 | 727 | if ( (!this.stars) ){ 728 | this.createStars(); 729 | } 730 | if (this.data.debug) console.log("starcount",starCount) 731 | if (!this.version) { 732 | this.stars.setAttribute('star-system', "count", starCount); 733 | } else { 734 | this.stars.getObject3D('mesh').geometry.setDrawRange(0, starCount); 735 | } 736 | 737 | if (this.data.debug) console.log("setStars", starCount, this.starlight) 738 | }, 739 | 740 | starCycle: 0, // dynamic 741 | fogValue: 0, // dynamic 742 | fogRangeMax: 1000, 743 | 744 | timestampToOrbitPercent(timestamp, x2) { 745 | this.msSinceStart = Date.now() - (timestamp||this.data.starttime); 746 | this.minSinceStart = this.msSinceStart / 1000 / 60; 747 | this.minIntoCycle = this.minSinceStart % (this.data.orbitduration * (x2 ? 2:1)); 748 | this.fractionOfCurrentCycle = this.minIntoCycle * ( 1 / (this.data.orbitduration * (x2 ? 2:1)) ); 749 | return this.fractionOfCurrentCycle; 750 | }, 751 | getOrbit(other) { 752 | // converts time passed into a fraction of 360, so 0,1,2...359,360,0,1...etc. 753 | // if (this.fractionOfCurrentCycle < 0) this.fractionOfCurrentCycle = -this.fractionOfCurrentCycle // might be wrong, just a test 754 | this.orbit = 360 * this.timestampToOrbitPercent(); 755 | return this.orbit; 756 | }, 757 | 758 | moon: false, 759 | moonSunSwitchFlag: false, 760 | 761 | getEighthStarLoop() { 762 | // dividing day and night each into four cycles, we get 8 total cycles. 763 | // returns a corresponding value from 0-7 for which eighth of day you are in 764 | // also returns what percent through that eighth you are 765 | 766 | // this one follows 20 degrees behind the 'real' loop 767 | // except for just before and after sunrise, when it returns modified values to adjust fog. 768 | // that part is a bit of a hack and should be adjusted, so: "todo" 769 | 770 | // 90 === 360/4 771 | 772 | this.quarterCount = Math.floor( this.starLoop / 90 ); 773 | this.lastEighthStarLoop = this.currentEighthStarLoop; 774 | this.currentEighthStarLoop = { 775 | which: this.moon ? this.quarterCount + 4 : this.quarterCount, 776 | percent: ((this.starLoop % 90) * (1/90)), 777 | } 778 | 779 | if (this.currentEighthStarLoop.which === 1 && this.currentEighthStarLoop.percent > .65) { 780 | this.currentEighthStarLoop.which = 2; // cheap way to prevent fog from increasing even after sunrise 781 | this.currentEighthStarLoop.percent = .2 // speed up to cause fog pre-sunrise early 782 | } 783 | else if (this.currentEighthStarLoop.which === 2) { 784 | this.currentEighthStarLoop.percent = this.currentEighthStarLoop.percent < .2 ? 785 | .2 : this.currentEighthStarLoop.percent; // matching up beginning of 2 with faux end of 1 786 | this.currentEighthStarLoop.percent = this.currentEighthStarLoop.percent < .5 ? 787 | this.currentEighthStarLoop.percent * 2 : this.currentEighthStarLoop.percent // speed up to cause shadows pre-sunrise early 788 | } 789 | 790 | if (this.data.debug && this.lastEighthStarLoop.which !== this.currentEighthStarLoop.which) { 791 | // console.log('new eighth star', { 792 | // eighth: this.currentEighth.which, 793 | // moon: this.moon, 794 | // starlight: this.starlight, 795 | // sunOrMoonUp: this.sunOrMoonUp(), 796 | // fog: this.fogValue, 797 | // intensity: this.lights.lightProps.intensity, 798 | // multiplier: this.lights.intensityMultiplier 799 | // }) 800 | 801 | // document.querySelector('#log').setAttribute('value', "fog: "+this.fogValue) 802 | // document.querySelector('#log').setAttribute('value', 's: ' + this.currentEighthStarLoop.which + "/8; e: " + this.currentEighth.which + "/8") 803 | } 804 | }, 805 | currentEighthStarLoop: { // dynamically set 806 | which: 2, 807 | percent: 0.001, 808 | }, 809 | 810 | secondHalf: false, 811 | getEighth() { 812 | // dividing day and night each into four cycles, we get 8 total cycles. 813 | // returns a corresponding value from 0-7 for which eighth of day you are in 814 | // also returns what percent through that eighth you are 815 | // on this loop, 0% @ 0/8 is sunrise, 100% @ 1/8 is sunset, 0% @ 4/8 is moonrise, 100% @ 5/8 is moonset 816 | 817 | // 90 === 360/4 818 | this.quarterCount = Math.floor( this.orbit / 90 ); 819 | this.lastEighth = this.currentEighth; 820 | if (this.data.mooncycle && 821 | (this.quarterCount === 0 && this.lastEighth.which === 3) 822 | || this.quarterCount === 0 && this.lastEighth.which === 7 823 | ) { 824 | this.secondHalf = !this.secondHalf 825 | } 826 | this.currentEighth = { 827 | which: this.secondHalf ? this.quarterCount + 4 : this.quarterCount, 828 | percent: ((this.orbit % 90) * (1/90)), 829 | } 830 | 831 | if (this.data.debug && this.lastEighth.which !== this.currentEighth.which) { 832 | console.log('new eighth', { 833 | eighth: this.currentEighth.which, 834 | moon: this.moon, 835 | starlight: this.starlight, 836 | fog: this.fogValue, 837 | intensity: this.lights.intensity, 838 | multiplier: this.lights.intensityMultiplier 839 | }) 840 | } 841 | }, 842 | currentEighth: { // dynamically set 843 | which: 0, 844 | percent: 0.001, 845 | }, 846 | 847 | setStarLoop() { 848 | // this was early code; would be nice to refactor this to this.inRange() 849 | if (this.orbit > this.data.starloopstart) { 850 | this.starLoop = this.orbit - this.data.starloopstart; 851 | } else { 852 | this.starLoop = this.orbit + (360 - this.data.starloopstart); 853 | } 854 | }, 855 | 856 | handleMoonCycle(orbit) { 857 | if (orbit < this.data.starloopstart && this.moonSunSwitchFlag) { 858 | this.moonSunSwitchFlag = false; 859 | } 860 | if (orbit > this.data.starloopstart && !this.moonSunSwitchFlag) { 861 | if (this.data.debug) console.log("switching sun/moon", orbit, 'moon:', this.moon) 862 | 863 | this.moon = !this.moon; 864 | 865 | this.moonSunSwitchFlag = true; 866 | if (this.moon) { 867 | if (this.data.debug) console.log('switch to moon cycle, would rotate scene for moonrise') 868 | // this.el.sceneEl.setAttribute('rotation', {y:this.data.moonriseOffset}) 869 | 870 | if (this.data.showstars) { 871 | // show stars right after sunset 872 | this.setStars(); 873 | 874 | if (this.data.debug) console.log('starlight on') 875 | } 876 | } else { 877 | if (this.data.debug) console.log('switch to sun cycle, would rotate scene for sunrise; feature removed') 878 | // this.el.sceneEl.setAttribute('rotation', {y:this.data.sunriseOffset}) 879 | } 880 | } 881 | 882 | // track star loop separately to have stars rise and fog come in offset just after sunset 883 | this.setStarLoop(); 884 | 885 | this.starCycle = this.starLoop * ( 1 / 180 ); // 0 -> 2 scale for the 0 -> 360 rotation; we do until 2 so that we can make it negative after 1. 886 | this.starCycle = this.starCycle > 1 ? 2 - this.starCycle : this.starCycle; 887 | 888 | // fog: 889 | // - 0 just after sunset, 890 | // - 0 just before sunrise, 891 | // - 1 at full day 892 | // - 1 at full night 893 | this.getEighthStarLoop(); 894 | 895 | if (this.currentEighthStarLoop.which === 2 || this.currentEighthStarLoop.which === 4) { 896 | // console.log("ready-to-detect-starlight-off", this.moon, this.starlight) 897 | // sunrise -> noon 898 | // or 899 | // sunset -> starlight 900 | // less fog (so, higher fog value) as percent of this.currentEighth goes up 901 | this.fogValue = this.currentEighthStarLoop.percent * this.fogRangeMax 902 | if (this.data.showstars && !this.moon && this.starlight) { 903 | this.setStars(0); 904 | } 905 | } 906 | else if (this.currentEighthStarLoop.which === 3 || this.currentEighthStarLoop.which === 1) { 907 | // noon -> sunset 908 | // or 909 | // dusk -> sunrise 910 | // more fog (so, lower fog value) as percent of this.currentEighthStarLoop goes up 911 | this.fogValue = (1 - this.currentEighthStarLoop.percent) * this.fogRangeMax 912 | } 913 | else { 914 | this.fogValue = this.fogRangeMax 915 | } 916 | // if (this.data.debug) console.log('fogValue', this.fogValue, this.currentEighthStarLoop.percent, this.fogRangeMax) 917 | if (this.fogValue < this.data.fogmin) this.fogValue = this.data.fogmin; 918 | 919 | if (!this.data.disablefog) { 920 | this.el.sceneEl.setAttribute('fog', 'far', this.fogValue); 921 | } 922 | }, 923 | starlight: false, // dynamically set, indicates if stars are an active light source, which they are for 3/4s of the day/night cycle 924 | howManyStars() { 925 | // can be used to cause stars to drop out one by one, fog creates more attractive effect, though. 926 | return this.data.starcount; 927 | // return (1 - Math.max(0, ( this.sunPos.y + 0.08) * 8)) * this.data.starcount; 928 | }, 929 | 930 | sunPosition: {x:0,y:0,z:-1}, // dynamically set 931 | sunPos: {x:0,y:0,z:0}, // dynamically set 932 | theta: Math.PI * (-.25), // putting it at .5 changes to a sun that goes straight overhead, but shader only supports movement in its very small range 933 | phi() {return (2 * Math.PI * ((this.orbit / 360) - 0.5))}, // percent of circumference 934 | // I think this 0.5 is the source of the offset by 2/8ths. it forces it to start at sunrise! // -.25 correspondes to noon, -0 corresponds to sunset 935 | 936 | lastTick: Date.now(), 937 | slowTickBuildup: 0, 938 | tolerance: -10, // normal ms delay 939 | notified: false, 940 | trackPerformance() { 941 | // NOTE: DOES NOT RUN UNLESS DEBUG ENABLED 942 | // console.log(this.throttle - (Date.now()-this.lastTick),this.tolerance) 943 | if (this.throttle - (Date.now()-this.lastTick) < this.tolerance) { 944 | // if (this.data.debug) console.warn("slow tick") 945 | this.slowTickBuildup++ 946 | } else if (this.slowTickBuildup > 0) { 947 | this.slowTickBuildup-- 948 | } 949 | if (this.slowTickBuildup > 5) { 950 | if (!this.notified) 951 | if (this.data.debug) console.error("demands may be too high, should increase throttle and reduce cycleduration", this.slowTickBuildup, this.throttle, this.throttle - (Date.now()-this.lastTick) ) 952 | this.notified = true; 953 | } 954 | else if (!this.slowTickBuildup) this.notified = false; 955 | 956 | this.lastTick = Date.now(); 957 | }, 958 | tick() { 959 | this.trackPerformance() 960 | this.orbit = this.getOrbit(); 961 | 962 | this.tickHandlers(); // why? so we can call 'tick' when changing settings but without updating the orbit, if we are paused. 963 | }, 964 | tickHandlers(){ 965 | if (this.data.mooncycle) { 966 | this.handleMoonCycle(this.orbit); 967 | } 968 | 969 | this.sunShaderTick(); 970 | this.lightSourcesTick(); 971 | }, 972 | sunShaderTick() { 973 | // draw the surface of the sphere with sunrise/sunset/moonlight colors, and 'visible' sun/moon 974 | this.sunPosition.x = Math.cos(this.phi()); 975 | this.sunPosition.y = Math.sin(this.phi()) * Math.sin(this.theta); 976 | 977 | this.setSunSkyMaterial() 978 | 979 | this.sky.setAttribute('material', this.sunSkyMaterial); 980 | }, 981 | sunSkyMaterial: { // dynamically set 982 | rayleigh: 1, 983 | luminance: 1, 984 | turbidity: .8, 985 | }, 986 | setSunSkyMaterial() { 987 | // adjusts sky factors to give variation between day and night 988 | // let label; 989 | 990 | if (this.currentEighthStarLoop.which === 1) { 991 | // 1 992 | // console.log("moon to sun", this.currentEighthStarLoop.which) 993 | // label = this.currentEighthStarLoop.which+' moon to sun; '; 994 | // from moon to sun 995 | this.sunSkyMaterial.rayleigh = this.data.moonrayleigh + (this.currentEighthStarLoop.percent * (this.data.sunrayleigh - this.data.moonrayleigh)); 996 | this.sunSkyMaterial.luminance = this.data.moonluminance + (this.currentEighthStarLoop.percent * (this.data.sunluminance - this.data.moonluminance)); 997 | this.sunSkyMaterial.turbidity = this.data.moonturbidity + (this.currentEighthStarLoop.percent * (this.data.sunturbidity - this.data.moonturbidity)); 998 | } 999 | else if (this.currentEighthStarLoop.which === 2 || this.currentEighthStarLoop.which === 3) { 1000 | // 2 3 1001 | // console.log("sun", this.currentEighthStarLoop.which) 1002 | // label = this.currentEighthStarLoop.which+' sun; '; 1003 | // straight sun 1004 | this.sunSkyMaterial.rayleigh = this.data.sunrayleigh 1005 | this.sunSkyMaterial.luminance = this.data.sunluminance 1006 | this.sunSkyMaterial.turbidity = this.data.sunturbidity 1007 | } 1008 | else if (this.currentEighthStarLoop.which === 4) { 1009 | // 4 1010 | // console.log('sun to moon') 1011 | // sun to moon 1012 | // label = this.currentEighthStarLoop.which+' sun to moon; '; 1013 | // 1 -(0.24266666666666703 * (1 - 0.1)) 1014 | this.sunSkyMaterial.rayleigh = (this.data.sunrayleigh - (this.currentEighthStarLoop.percent * (this.data.sunrayleigh - this.data.moonrayleigh)) ); 1015 | this.sunSkyMaterial.luminance = (this.data.sunluminance - (this.currentEighthStarLoop.percent * (this.data.sunluminance - this.data.moonluminance)) ); 1016 | this.sunSkyMaterial.turbidity = (this.data.sunturbidity - (this.currentEighthStarLoop.percent * (this.data.sunturbidity - this.data.moonturbidity)) ); 1017 | // debugger; 1018 | } 1019 | else if (this.currentEighthStarLoop.which) { 1020 | // 5 6 7 0 1021 | // console.log('moon') 1022 | // straight moon 1023 | // label = this.currentEighthStarLoop.which+' moon; '; 1024 | this.sunSkyMaterial.rayleigh = this.data.moonrayleigh 1025 | this.sunSkyMaterial.luminance = this.data.moonluminance 1026 | this.sunSkyMaterial.turbidity = this.data.moonturbidity 1027 | } 1028 | 1029 | // document.querySelector('#log2').setAttribute('value', label+"r: " +`${this.sunSkyMaterial.rayleigh}`+"; l:" +this.sunSkyMaterial.luminance+"; t:"+this.sunSkyMaterial.turbidity) 1030 | // document.querySelector('#log').setAttribute('value', `sR:${this.data.sunrayleigh} SL%:${Math.floor(this.currentEighthStarLoop.percent * 1000) / 1000} mR:${this.data.moonrayleigh}`) 1031 | // console.log(this.currentEighthStarLoop.which, this.sunSkyMaterial.rayleigh, `${this.data.sunrayleigh} -(${this.currentEighthStarLoop.percent} * (${this.data.sunrayleigh} - ${this.data.moonrayleigh}))`) 1032 | 1033 | this.sunSkyMaterial.sunPosition = this.sunPosition // this.version === 0 ? this.sunPosition : this.sunPos 1034 | }, 1035 | 1036 | glc: { 1037 | fogRatios: [ 1, 0.5, 0.22, 0.1, 0.05, 0, -1], 1038 | lightColors: ['#C0CDCF', '#81ADC5', '#525e62', '#2a2d2d', '#141616', '#000', '#e3e1aa' /*'#fffed9'*/], 1039 | starBrightRange: {start:{which:3},end:{which:6}} 1040 | // moonsky: '#e3df8a', // '#fffcab', 1041 | }, 1042 | 1043 | // returns a light color from a specific sky type and sun height 1044 | getLightColor: function (getLight=false) { // if false, we're getting fog, not light 1045 | // let lightColor; 1046 | 1047 | this.glc.sunHeight = Math.min(1, this.sunPosition.y); // to make sure it's never a value higher than 1, which shouldn't happen under normal conditions anyways 1048 | this.glc.ratioA; 1049 | this.glc.ratioB; 1050 | this.glc.i = 0; 1051 | for (; this.glc.i < this.glc.fogRatios.length; this.glc.i++){ 1052 | if (this.glc.sunHeight > this.glc.fogRatios[this.glc.i]){ 1053 | if (this.moon && this.glc.sunHeight > 0) { // moon is up 1054 | this.glc.ratioA = -1; // lock to last two night colors while moon is up, instead of going through daylight colors 1055 | this.glc.ratioB = 0; 1056 | // never understood why this method wouldn't work, but found a different workaround that is doing well now. 1057 | // var c1 = new THREE.Color(this.glc.lightColors[this.glc.lightColors.length-1]); 1058 | // var c2 = new THREE.Color(this.glc.moonsky); 1059 | this.glc.i = this.glc.fogRatios.length-1; 1060 | } else { // sun or stars without moon 1061 | this.glc.ratioA = this.glc.fogRatios[this.glc.i]; 1062 | this.glc.ratioB = this.glc.fogRatios[this.glc.i - 1]; 1063 | } 1064 | this.glc.c1 = new THREE.Color(this.glc.lightColors[this.glc.i - 1]); // index 0 - 1? never happens-- it's never > 1 (max .7), so index 0 is only ever c1, can never be c2 missing a c1 1065 | this.glc.c2 = new THREE.Color(this.glc.lightColors[this.glc.i]); 1066 | 1067 | this.glc.a = ((this.glc.sunHeight) - this.glc.ratioA) / (this.glc.ratioB - this.glc.ratioA); // how far are we on the path from color 1 to color 2 1068 | this.glc.a = this.moon && this.glc.sunHeight > 0 ? (1 - (this.glc.a-1)) : this.glc.a; // handle negative sun range values 1069 | if (this.inRange(this.glc.starBrightRange)) { 1070 | // lock at peak white while the stars are up 1071 | if (this.glc.a > .29) this.glc.a = .29 1072 | // this hard coded value was the observed peak at 'midnight' of a given sun cycle. 1073 | // the range is from moon-midnight to sun-midnight, preventing drops towards black pre-moonrise and post-moon-noon 1074 | } 1075 | // console.log(this.inRange(this.glc.starBrightRange)) 1076 | // if (this.glc.sunHeight < 0) this.glc.a = .99; 1077 | // if (this.glc.sunHeight < 0) this.glc.a = this.glc.a-1//.99; 1078 | 1079 | // document.querySelector('#log').setAttribute('value', 1080 | // this.glc.ratioA + "|" + this.glc.ratioB + "|" + 1081 | // ((this.moon && this.glc.sunHeight > 0) ? this.glc.lightColors[this.glc.lightColors.length-1] : this.glc.lightColors[this.glc.i - 1]) + "|" + 1082 | // " a:"+ this.cleanNumber(this.glc.a,100,!!'fixedlengthstring') + "|"+ 1083 | // (/*(this.moon && this.glc.sunHeight > 0) ? this.glc.moonsky : */this.glc.lightColors[this.glc.i]) +"|"+ 1084 | // // '#' + lightColor.getHexString() + 1085 | // ((this.moon && this.glc.sunHeight > 0) ? "| moon" : "| day") 1086 | // ) 1087 | 1088 | this.glc.c2.lerp(this.glc.c1, this.glc.a); 1089 | this.glc.lightColor = this.glc.c2; 1090 | break; 1091 | } 1092 | } 1093 | 1094 | // dim down the color 1095 | this.glc.lightColor.multiplyScalar(getLight ? 1: 0.9); 1096 | 1097 | if (!getLight) { // therefore, getting fog 1098 | // mix fog a bit with ground color; for light, this happens within the light component 1099 | // lightColor.lerp(new THREE.Color(this.data.groundColor), 0.3); 1100 | // removed, because it was always too 'bright'--wanted it more 'black' at night, not a halo 1101 | } 1102 | 1103 | // document.querySelector('#color-show').setAttribute('material','color','#' + this.glc.lightColor.getHexString()) 1104 | // document.querySelector('#log2').setAttribute('value', this.glc.lightColor.getHexString()) 1105 | return '#' + this.glc.lightColor.getHexString(); 1106 | }, 1107 | 1108 | inRange(startEnd){ 1109 | // utility function I built at the end, only used in one place so far, 1110 | // but actually a lot of code should be refactored to use this for increased clarity 1111 | // 1112 | // takes in object of type: 1113 | // {start:{which:1,percent:.33}, end:{which:4,percent:.66}} 1114 | // (percent is optional) 1115 | // limits are inclusive, so "start.which = 1; end.which = 4" (without percent) means from 0% of 1 until 100% of 4. 1116 | // 1117 | // does not currently allow start position that is after end position (start/end that loops through end) 1118 | // to accomplish that, use two seperate ranges, from which=0 to end, and from start to which=8 1119 | if (startEnd.start.which > this.currentEighth.which || startEnd.end.which < this.currentEighth.which) { 1120 | return false 1121 | } 1122 | if (!startEnd.start.hasOwnProperty('percent') && !startEnd.end.hasOwnProperty('percent')) { 1123 | return true 1124 | } 1125 | if (startEnd.start.percent > this.currentEighth.percent || startEnd.end.percent < this.currentEighth.percent) { 1126 | return false 1127 | } 1128 | else { 1129 | return true 1130 | } 1131 | }, 1132 | 1133 | lights: { // set dynamically 1134 | posSurface: { 1135 | z: -1, 1136 | }, 1137 | posBeam: {}, 1138 | posHemi: {}, 1139 | 1140 | lightHemi: {}, 1141 | lightBeam: {}, 1142 | }, 1143 | 1144 | ranges: { 1145 | starBright: {start:{which:3},end:{which:6}}, // sun-midnight to moon-midnight; when the stars shine 1146 | extendedStarRange: {start:{which:2},end:{which:7}}, // includes the wax on and wane out of star range just after sunset until just before sunrise 1147 | 1148 | daylight: {start:{which:0},end:{which:1}}, // when sun is shining 1149 | moonlight: {start:{which:4},end:{which:5}}, // when sun is shining 1150 | }, 1151 | 1152 | lightSourcesTick() { 1153 | 1154 | // console.log(this.timeOfDay()) 1155 | 1156 | // this.offset = this.moon ? this.data.moonriseOffset : this.data.sunriseOffset; 1157 | // this.el.setAttribute('rotation', 'y', -this.offset) 1158 | 1159 | // update light colors and intensities 1160 | this.sunPos = new THREE.Vector3(this.sunPosition.x, this.sunPosition.y, this.sunPosition.z) 1161 | this.sunPos.normalize(); 1162 | 1163 | if (!this.data.disablealllighting) { 1164 | this.lights.intensityMultiplier = 1165 | // !this.moon && this.sunOrMoonUp() ? 1166 | this.inRange(this.ranges.daylight) ? 1167 | this.data.sunintensity : // daylight 1168 | // this.moon && this.sunOrMoonUp() ? 1169 | this.inRange(this.ranges.moonlight) ? 1170 | this.data.moonintensity : // moon is up 1171 | // this.starlight && !this.sunOrMoonUp() ? 1172 | this.inRange(this.ranges.extendedStarRange) ? 1173 | 0.005 : // stars pre-and-post moon 1174 | 1 // shouldn't happen, flash to make reasoning error obvious 1175 | if (this.data.debug && this.lights.intensityMultiplier===1) console.warn("light intensity calc problem") 1176 | this.lights.intensity = this.inRange(this.ranges.extendedStarRange) ? this.data.starlightintensity : (0.1 + this.sunPos.y * this.lights.intensityMultiplier); 1177 | // this.lights.lightProps.intensity = this.lights.lightProps.intensity < this.data.starlightintensity ? this.data.starlightintensity : this.lights.lightProps.intensity; 1178 | // document.getElementById('lamp').setAttribute('light',this.lights.lightProps); 1179 | // console.log(this.lights.lightProps.intensity, this.lights.intensityMultiplier) 1180 | 1181 | 1182 | this.lights.lightHemi.color = this.getLightColor(1); 1183 | this.lights.lightBeam.color = this.lights.lightHemi.color 1184 | 1185 | // document.querySelector('#log2').setAttribute('value', this.lights.intensityMultiplier + " m=>i " + this.cleanNumber(this.lights.lightProps.intensity) + " |s: " + this.cleanNumber(this.sunPosition.y) + " |c " + this.lights.lightProps.color) 1186 | } 1187 | // adding to garbage collection overhead--make all objects here into local re-used objects 1188 | if (this.data.showsurfacelight) { 1189 | this.lights.posSurface.x = -this.sunPosition.x; 1190 | this.lights.posSurface.y = this.sunPosition.y 1191 | this.sunlight.setAttribute('position', this.lights.posSurface); // new experimental version 1192 | this.sunlight.setAttribute('light', this.lights.lightHemi); 1193 | } 1194 | 1195 | if (this.data.showhemilight || this.data.showshadowlight) { 1196 | this.sunshaderToSunbeam(); 1197 | } 1198 | 1199 | if (this.data.showhemilight) { 1200 | this.lights.lightHemi.intensity = this.lights.intensity < this.data.starlightintensity ? this.data.starlightintensity : this.lights.intensity; 1201 | // delete this.lights.lightProps.castShadow; // delete because hemilight complains 1202 | this.hemilight.setAttribute('light', this.lights.lightHemi); 1203 | } 1204 | 1205 | if (this.data.showshadowlight) { 1206 | this.sunbeam.setAttribute('position', this.toD3); 1207 | this.lights.lightBeam.castShadow = this.sunOrMoonUp(); // should actually specify eighths where sun/moon are up. 1208 | this.lights.lightBeam.intensity = this.sunOrMoonUp() ? this.lights.intensity : 0 1209 | this.sunbeam.setAttribute('light', this.lights.lightBeam); 1210 | } 1211 | }, 1212 | 1213 | cN: 0, 1214 | cleanNumber(n, f=100, fixedLengthString=false) { 1215 | this.cN = Math.floor(n * f)/f; 1216 | // console.log(f, this.cN, this.cN.toString().length < f.toString().length) 1217 | return fixedLengthString ? this.fixedLength(this.cN,f.toString().length,"0") : this.cN; 1218 | }, 1219 | 1220 | fixedLength(input,length,padChar=" ") { 1221 | let output = input.toString(); 1222 | if (typeof input === "number" && input < 1 && input != 0) { 1223 | // console.log(output) 1224 | output = "."+output.split('.')[1].toString() 1225 | } 1226 | // console.log(input,output) 1227 | while(output.length < length) { 1228 | output+=padChar; 1229 | } 1230 | return output; 1231 | }, 1232 | sunOrMoonUp() { 1233 | this.getEighth(); 1234 | return this.currentEighth.which === 0 || this.currentEighth.which === 1 || this.currentEighth.which === 4 || this.currentEighth.which === 5; 1235 | }, 1236 | toD3: { x:0, y:0, z:0 }, // dynamically set 1237 | sunshaderToSunbeam(/*x,y*/) { 1238 | // using the same 0-360 orbit input, get x/y/z 3d vector to (mostly) match sun-sky's sun texture position 1239 | // note that the shader actually allows a very limited range for the sun/moon, and will ignore inputs outside of its range 1240 | // this function won't, though, and trying to force the moon/sun beyond range will just cause the light source to go out of sync. 1241 | this.toD3.x = ( (this.data.sunbeamdistance) * Math.sin(this.theta) * Math.cos(this.phi()) ); 1242 | this.toD3.y = ( this.data.sunbeamdistance) * Math.sin(this.theta) * Math.sin(this.phi() ); 1243 | this.toD3.z = -( (this.data.sunbeamdistance) * Math.cos(this.theta) ); 1244 | }, 1245 | 1246 | moveSunrise(degrees) { 1247 | // note on rotation: you can add scene rotation, and then negatively rotate the a-sun-sky sphere. 1248 | // rotating that sphere will rotate the path of the child sunbeam, but will not affect the shader, which is why you have to rotate the scene. 1249 | // rotating scene can have strange side effects, though, if anything is placed with world position it will suddenly be out of place. 1250 | if (this.data.debug) console.warn("rotating a-scene, likely problematic") 1251 | // unfortunately, I don't yet see any other way to change the position of the sunrise... 1252 | AFRAME.scenes[0].sceneEl.setAttribute('rotation','y',-degrees) 1253 | this.sky.setAttribute('rotation','y',-degrees) 1254 | }, 1255 | }); 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | // 1262 | // 1263 | // sky shader, taken from A-Frame master repo's examples folders, as of V 1.2.0 1264 | // 1265 | // 1266 | /* global AFRAME */ 1267 | AFRAME.registerShader('sky', { 1268 | schema: { 1269 | luminance: { type: 'number', default: 1, min: 0, max: 2, is: 'uniform' }, 1270 | turbidity: { type: 'number', default: 2, min: 0, max: 20, is: 'uniform' }, 1271 | rayleigh: { type: 'number', default: 1, min: 0, max: 4, is: 'uniform' }, 1272 | mieCoefficient: { type: 'number', default: 0.005, min: 0, max: 0.1, is: 'uniform' }, 1273 | mieDirectionalG: { type: 'number', default: 0.8, min: 0, max: 1, is: 'uniform' }, 1274 | sunPosition: { type: 'vec3', default: '0 0 -1', is: 'uniform' } 1275 | }, 1276 | 1277 | vertexShader: [ 1278 | 'varying vec3 vWorldPosition;', 1279 | 1280 | 'void main() {', 1281 | 1282 | 'vec4 worldPosition = modelMatrix * vec4( position, 1.0 );', 1283 | 'vWorldPosition = worldPosition.xyz;', 1284 | 1285 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 1286 | 1287 | '}' 1288 | 1289 | ].join('\n'), 1290 | 1291 | fragmentShader: [ 1292 | 1293 | 'uniform sampler2D skySampler;', 1294 | 'uniform vec3 sunPosition;', 1295 | 'varying vec3 vWorldPosition;', 1296 | 1297 | 'vec3 cameraPos = vec3(0., 0., 0.);', 1298 | 1299 | 'uniform float luminance;', 1300 | 'uniform float turbidity;', 1301 | 'uniform float rayleigh;', 1302 | 'uniform float mieCoefficient;', 1303 | 'uniform float mieDirectionalG;', 1304 | 1305 | '// constants for atmospheric scattering', 1306 | 'const float e = 2.71828182845904523536028747135266249775724709369995957;', 1307 | 'const float pi = 3.141592653589793238462643383279502884197169;', 1308 | 1309 | 'const float n = 1.0003; // refractive index of air', 1310 | 'const float N = 2.545E25; // number of molecules per unit volume for air at', 1311 | '// 288.15K and 1013mb (sea level -45 celsius)', 1312 | 'const float pn = 0.035; // depolatization factor for standard air', 1313 | 1314 | '// wavelength of used primaries, according to preetham', 1315 | 'const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);', 1316 | 1317 | '// mie stuff', 1318 | '// K coefficient for the primaries', 1319 | 'const vec3 K = vec3(0.686, 0.678, 0.666);', 1320 | 'const float v = 4.0;', 1321 | 1322 | '// optical length at zenith for molecules', 1323 | 'const float rayleighZenithLength = 8.4E3;', 1324 | 'const float mieZenithLength = 1.25E3;', 1325 | 'const vec3 up = vec3(0.0, 1.0, 0.0);', 1326 | 1327 | 'const float EE = 1000.0;', 1328 | 'const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;', 1329 | '// 66 arc seconds -> degrees, and the cosine of that', 1330 | 1331 | '// earth shadow hack', 1332 | 'const float cutoffAngle = pi/1.95;', 1333 | 'const float steepness = 1.5;', 1334 | 1335 | 'vec3 totalRayleigh(vec3 lambda)', 1336 | '{', 1337 | 'return (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn));', 1338 | '}', 1339 | 1340 | // see http://blenderartists.org/forum/showthread.php?321110-Shaders-and-Skybox-madness 1341 | '// A simplied version of the total Rayleigh scattering to works on browsers that use ANGLE', 1342 | 'vec3 simplifiedRayleigh()', 1343 | '{', 1344 | 'return 0.0005 / vec3(94, 40, 18);', 1345 | '}', 1346 | 1347 | 'float rayleighPhase(float cosTheta)', 1348 | '{ ', 1349 | 'return (3.0 / (16.0*pi)) * (1.0 + pow(cosTheta, 2.0));', 1350 | '}', 1351 | 1352 | 'vec3 totalMie(vec3 lambda, vec3 K, float T)', 1353 | '{', 1354 | 'float c = (0.2 * T ) * 10E-18;', 1355 | 'return 0.434 * c * pi * pow((2.0 * pi) / lambda, vec3(v - 2.0)) * K;', 1356 | '}', 1357 | 1358 | 'float hgPhase(float cosTheta, float g)', 1359 | '{', 1360 | 'return (1.0 / (4.0*pi)) * ((1.0 - pow(g, 2.0)) / pow(1.0 - 2.0*g*cosTheta + pow(g, 2.0), 1.5));', 1361 | '}', 1362 | 1363 | 'float sunIntensity(float zenithAngleCos)', 1364 | '{', 1365 | 'return EE * max(0.0, 1.0 - exp(-((cutoffAngle - acos(zenithAngleCos))/steepness)));', 1366 | '}', 1367 | 1368 | '// Filmic ToneMapping http://filmicgames.com/archives/75', 1369 | 'float A = 0.15;', 1370 | 'float B = 0.50;', 1371 | 'float C = 0.10;', 1372 | 'float D = 0.20;', 1373 | 'float E = 0.02;', 1374 | 'float F = 0.30;', 1375 | 'float W = 1000.0;', 1376 | 1377 | 'vec3 Uncharted2Tonemap(vec3 x)', 1378 | '{', 1379 | 'return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;', 1380 | '}', 1381 | 1382 | 'void main() ', 1383 | '{', 1384 | 'float sunfade = 1.0-clamp(1.0-exp((sunPosition.y/450000.0)),0.0,1.0);', 1385 | 1386 | 'float rayleighCoefficient = rayleigh - (1.0* (1.0-sunfade));', 1387 | 1388 | 'vec3 sunDirection = normalize(sunPosition);', 1389 | 1390 | 'float sunE = sunIntensity(dot(sunDirection, up));', 1391 | 1392 | '// extinction (absorbtion + out scattering) ', 1393 | '// rayleigh coefficients', 1394 | 1395 | 'vec3 betaR = simplifiedRayleigh() * rayleighCoefficient;', 1396 | 1397 | '// mie coefficients', 1398 | 'vec3 betaM = totalMie(lambda, K, turbidity) * mieCoefficient;', 1399 | 1400 | '// optical length', 1401 | '// cutoff angle at 90 to avoid singularity in next formula.', 1402 | 'float zenithAngle = acos(max(0.0, dot(up, normalize(vWorldPosition - cameraPos))));', 1403 | 'float sR = rayleighZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));', 1404 | 'float sM = mieZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));', 1405 | 1406 | '// combined extinction factor ', 1407 | 'vec3 Fex = exp(-(betaR * sR + betaM * sM));', 1408 | 1409 | '// in scattering', 1410 | 'float cosTheta = dot(normalize(vWorldPosition - cameraPos), sunDirection);', 1411 | 1412 | 'float rPhase = rayleighPhase(cosTheta*0.5+0.5);', 1413 | 'vec3 betaRTheta = betaR * rPhase;', 1414 | 1415 | 'float mPhase = hgPhase(cosTheta, mieDirectionalG);', 1416 | 'vec3 betaMTheta = betaM * mPhase;', 1417 | 1418 | 'vec3 Lin = pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * (1.0 - Fex),vec3(1.5));', 1419 | 'Lin *= mix(vec3(1.0),pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * Fex,vec3(1.0/2.0)),clamp(pow(1.0-dot(up, sunDirection),5.0),0.0,1.0));', 1420 | 1421 | '//nightsky', 1422 | 'vec3 direction = normalize(vWorldPosition - cameraPos);', 1423 | 'float theta = acos(direction.y); // elevation --> y-axis, [-pi/2, pi/2]', 1424 | 'float phi = atan(direction.z, direction.x); // azimuth --> x-axis [-pi/2, pi/2]', 1425 | 'vec2 uv = vec2(phi, theta) / vec2(2.0*pi, pi) + vec2(0.5, 0.0);', 1426 | '// vec3 L0 = texture2D(skySampler, uv).rgb+0.1 * Fex;', 1427 | 'vec3 L0 = vec3(0.1) * Fex;', 1428 | 1429 | '// composition + solar disc', 1430 | 'float sundisk = smoothstep(sunAngularDiameterCos,sunAngularDiameterCos+0.00002,cosTheta);', 1431 | 'L0 += (sunE * 19000.0 * Fex)*sundisk;', 1432 | 1433 | 'vec3 whiteScale = 1.0/Uncharted2Tonemap(vec3(W));', 1434 | 1435 | 'vec3 texColor = (Lin+L0); ', 1436 | 'texColor *= 0.04 ;', 1437 | 'texColor += vec3(0.0,0.001,0.0025)*0.3;', 1438 | 1439 | 'float g_fMaxLuminance = 1.0;', 1440 | 'float fLumScaled = 0.1 / luminance; ', 1441 | 'float fLumCompressed = (fLumScaled * (1.0 + (fLumScaled / (g_fMaxLuminance * g_fMaxLuminance)))) / (1.0 + fLumScaled); ', 1442 | 1443 | 'float ExposureBias = fLumCompressed;', 1444 | 1445 | 'vec3 curr = Uncharted2Tonemap((log2(2.0/pow(luminance,4.0)))*texColor);', 1446 | 'vec3 color = curr*whiteScale;', 1447 | 1448 | 'vec3 retColor = pow(color,vec3(1.0/(1.2+(1.2*sunfade))));', 1449 | 1450 | 'gl_FragColor.rgb = retColor;', 1451 | 1452 | 'gl_FragColor.a = 1.0;', 1453 | '}' 1454 | ].join('\n') 1455 | }); 1456 | --------------------------------------------------------------------------------