├── 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://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 |
9 |
10 | 
11 | 
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 |
--------------------------------------------------------------------------------