Skip to content

Commit

Permalink
[fix] effect related fixes (split maps, shadows, timeline) (#2396)
Browse files Browse the repository at this point in the history
Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
  • Loading branch information
igorDykhta committed Oct 24, 2023
1 parent 5e7dd9b commit a36ec68
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 42 deletions.
21 changes: 12 additions & 9 deletions src/components/src/effects/effect-time-configurator.tsx
Expand Up @@ -92,20 +92,21 @@ export default function EffectTimeConfiguratorFactory(
onTimeModeChange,
intl
}: EffectTimeConfiguratorProps) => {
const [selectedDate, selectedTimeString] = useMemo(() => {
const [dateOnly, selectedTimeString] = useMemo(() => {
const date = new Date(timestamp);

const h = date.getHours();
const m = date.getMinutes();
const h = date.getUTCHours();
const m = date.getUTCMinutes();
const timeString = `${h < 10 ? `0${h}` : `${h}`}:${m < 10 ? `0${m}` : `${m}`}`;

date.setUTCHours(0, 0, 0);
return [date, timeString];
}, [timestamp]);

const timeSliderValue = useMemo(() => {
const base = new Date(timestamp).setHours(0, 0, 0).valueOf();
const base = dateOnly.valueOf();
return clamp([0, 1], (parseInt(timestamp) - base) / DAY_SLIDER_RANGE);
}, [timestamp, selectedDate]);
}, [timestamp, dateOnly]);

const timeSliderProps = useMemo(() => {
return {
Expand All @@ -115,7 +116,7 @@ export default function EffectTimeConfiguratorFactory(
range: [0, 1],
value0: 0,
onChange: value => {
const start = new Date(timestamp).setHours(0, 0, 0).valueOf();
const start = new Date(timestamp).setUTCHours(0, 0, 0).valueOf();
onDateTimeChange(Math.floor(start + DAY_SLIDER_RANGE * clamp([0, 0.9999], value[1])));
},
showInput: false,
Expand All @@ -125,15 +126,17 @@ export default function EffectTimeConfiguratorFactory(

const setDate = useCallback(
newDate => {
onDateTimeChange(Math.floor(newDate.valueOf() + DAY_SLIDER_RANGE * timeSliderValue));
const adjustedTime = newDate.valueOf() - newDate.getTimezoneOffset() * 1000 * 60;
const newTimestamp = Math.floor(adjustedTime + DAY_SLIDER_RANGE * timeSliderValue);
onDateTimeChange(newTimestamp);
},
[timeSliderValue, onDateTimeChange]
);

const setTime = useCallback(
time => {
const conf = time.split(':');
const start = new Date(timestamp).setHours(conf[0], conf[1]).valueOf();
const start = new Date(timestamp).setUTCHours(conf[0], conf[1]).valueOf();
onDateTimeChange(start);
},
[timestamp, onDateTimeChange]
Expand Down Expand Up @@ -167,7 +170,7 @@ export default function EffectTimeConfiguratorFactory(
<FormattedMessage id={'effectManager.pickCurrrentTime'} />
</Tooltip>
</StyledButton>
<StyledDatePicker value={selectedDate} onChange={setDate} />
<StyledDatePicker value={dateOnly} onChange={setDate} />
<StyledTimePicker
value={selectedTimeString}
onChange={setTime}
Expand Down
1 change: 1 addition & 0 deletions src/components/src/index.ts
Expand Up @@ -265,6 +265,7 @@ export {default as EffectListFactory} from './effects/effect-list';
export {default as SidePanelTitleFactory} from './effects/side-panel-title';
export {default as EffectTypeSelectorFactory} from './effects/effect-type-selector';
export {default as EffectConfiguratorFactory} from './effects/effect-configurator';
export {default as EffectTimeConfiguratorFactory} from './effects/effect-time-configurator';

export {default as HowToButton} from './side-panel/layer-panel/how-to-button';
// eslint-disable-next-line prettier/prettier
Expand Down
15 changes: 11 additions & 4 deletions src/components/src/map-container.tsx
Expand Up @@ -540,9 +540,14 @@ export default function MapContainerFactory(
}
}

_isOKToRenderEffects() {
// TODO a hack to prevent effect preRender without valid generated viewports
return Boolean(this._deck?.viewManager?._viewports?.length);
/**
* 1) Allow effects only for the first view.
* 2) Prevent effect:preRender call without valid generated viewports.
* @param viewIndex View index.
* @returns Returns true if effects can be used.
*/
_isOKToRenderEffects(viewIndex?: number): boolean {
return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
}

_onBeforeRender = ({gl}) => {
Expand Down Expand Up @@ -791,7 +796,9 @@ export default function MapContainerFactory(
};
}

const effects = this._isOKToRenderEffects() ? computeDeckEffects({visState, mapState}) : [];
const effects = this._isOKToRenderEffects(index)
? computeDeckEffects({visState, mapState})
: [];

const views = deckGlProps?.views
? deckGlProps?.views()
Expand Down
1 change: 1 addition & 0 deletions src/effects/package.json
Expand Up @@ -32,6 +32,7 @@
"dependencies": {
"suncalc": "^1.9.0",
"@deck.gl/core": "^8.9.12",
"@luma.gl/core": "^8.5.19",
"@luma.gl/shadertools": "^8.5.19",
"@kepler.gl/utils": "3.0.0-alpha.1",
"@kepler.gl/constants": "3.0.0-alpha.1",
Expand Down
132 changes: 132 additions & 0 deletions src/effects/src/custom-deck-lighting-effect.ts
@@ -0,0 +1,132 @@
// @ts-nocheck This is a hack, don't check types

import {console as Console} from 'global/window';
import {LightingEffect, shadow} from '@deck.gl/core';
import {Texture2D, ProgramManager} from '@luma.gl/core';

/**
* Inserts shader code before detected part.
* @param {string} vs Original shader code.
* @param {string} type Debug string.
* @param {string} insertBeforeText Text chunk to insert before.
* @param {string} textToInsert Text to insert.
* @returns Modified shader code.
*/
export function insertBefore(vs, type, insertBeforeText, textToInsert) {
const at = vs.indexOf(insertBeforeText);
if (at < 0) {
Console.error(`Cannot edit ${type} layer shader`);
return vs;
}

return vs.slice(0, at) + textToInsert + vs.slice(at);
}

const CustomShadowModule = shadow ? {...shadow} : undefined;

/**
* Custom shadow module
* 1) Add u_outputUniformShadow uniform
* 2) always produce full shadow when the uniform is set to true.
*/
CustomShadowModule.fs = insertBefore(
CustomShadowModule.fs,
'custom shadow #1',
'uniform vec4 shadow_uColor;',
'uniform bool u_outputUniformShadow;'
);

CustomShadowModule.fs = insertBefore(
CustomShadowModule.fs,
'custom shadow #1',
'vec4 rgbaDepth = texture2D(shadowMap, position.xy);',
'if(u_outputUniformShadow) return 1.0;'
);

CustomShadowModule.getUniforms = (opts = {}, context = {}) => {
const u = shadow.getUniforms(opts, context);
if (opts.outputUniformShadow !== undefined) {
u['u_outputUniformShadow'] = opts.outputUniformShadow;
}
return u;
};

/**
* Custom LightingEffect
* 1) adds CustomShadowModule
* 2) pass outputUniformShadow as module parameters
* 3) properly removes CustomShadowModule
*/
class CustomDeckLightingEffect extends LightingEffect {
constructor(props) {
super(props);
this.useOutputUniformShadow = false;
}

preRender(gl, {layers, layerFilter, viewports, onViewportActive, views}) {
if (!this.shadow) return;

// create light matrix every frame to make sure always updated from light source
this.shadowMatrices = this._calculateMatrices();

if (this.shadowPasses.length === 0) {
this._createShadowPasses(gl);
}
if (!this.programManager) {
this.programManager = ProgramManager.getDefaultProgramManager(gl);
if (CustomShadowModule) {
this.programManager.addDefaultModule(CustomShadowModule);
}
}

if (!this.dummyShadowMap) {
this.dummyShadowMap = new Texture2D(gl, {
width: 1,
height: 1
});
}

for (let i = 0; i < this.shadowPasses.length; i++) {
const shadowPass = this.shadowPasses[i];
shadowPass.render({
layers,
layerFilter,
viewports,
onViewportActive,
views,
moduleParameters: {
shadowLightId: i,
dummyShadowMap: this.dummyShadowMap,
shadowMatrices: this.shadowMatrices,
useOutputUniformShadow: false
}
});
}
}

getModuleParameters(layer) {
const parameters = super.getModuleParameters(layer);
parameters.outputUniformShadow = this.outputUniformShadow;
return parameters;
}

cleanup() {
for (const shadowPass of this.shadowPasses) {
shadowPass.delete();
}
this.shadowPasses.length = 0;
this.shadowMaps.length = 0;

if (this.dummyShadowMap) {
this.dummyShadowMap.delete();
this.dummyShadowMap = null;
}

if (this.shadow && this.programManager) {
this.programManager.removeDefaultModule(CustomShadowModule);
this.programManager = null;
}
}
}

export default CustomDeckLightingEffect;
9 changes: 3 additions & 6 deletions src/effects/src/lighting-effect.ts
@@ -1,14 +1,11 @@
import {
LightingEffect as DeckLightingEffect,
AmbientLight,
_SunLight as SunLight
} from '@deck.gl/core';
import {AmbientLight, _SunLight as SunLight} from '@deck.gl/core';

import {LIGHT_AND_SHADOW_EFFECT, DEFAULT_LIGHT_AND_SHADOW_PROPS} from '@kepler.gl/constants';
import {normalizeColor} from '@kepler.gl/utils';
import {EffectConfig, EffectParamsPartial} from '@kepler.gl/types';

import Effect from './effect';
import CustomDeckLightingEffect from './custom-deck-lighting-effect';

const LIGHT_AND_SHADOW_EFFECT_DESC = {
...LIGHT_AND_SHADOW_EFFECT,
Expand Down Expand Up @@ -38,7 +35,7 @@ class LightingEffect extends Effect {
_shadow: true
});

this.deckEffect = new DeckLightingEffect({
this.deckEffect = new CustomDeckLightingEffect({
ambientLight,
sunLight
});
Expand Down
70 changes: 48 additions & 22 deletions src/utils/src/effect-utils.ts
Expand Up @@ -3,7 +3,12 @@ import SunCalc from 'suncalc';

import {PostProcessEffect} from '@deck.gl/core/typed';

import {LIGHT_AND_SHADOW_EFFECT, LIGHT_AND_SHADOW_EFFECT_TIME_MODES} from '@kepler.gl/constants';
import {
LIGHT_AND_SHADOW_EFFECT,
LIGHT_AND_SHADOW_EFFECT_TIME_MODES,
FILTER_TYPES,
FILTER_VIEW_TYPES
} from '@kepler.gl/constants';
import {findById} from './utils';
import {VisState} from '@kepler.gl/schemas';
import {MapState, Effect} from '@kepler.gl/types';
Expand All @@ -22,27 +27,10 @@ export function computeDeckEffects({
})
.filter(effect => Boolean(effect && effect.config.isEnabled && effect.deckEffect)) as Effect[];

const lightShadowEffect = effects.find(effect => effect.type === LIGHT_AND_SHADOW_EFFECT.type);
if (lightShadowEffect) {
const {timestamp, timeMode} = lightShadowEffect.config.params;

if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.current) {
lightShadowEffect.deckEffect.directionalLights[0].timestamp = Date.now();
} else if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.animation) {
// TODO: find an easy way to get current animation time
const filter = visState.filters.find(filter => filter.fieldType === 'timestamp');
if (filter) {
lightShadowEffect.deckEffect.directionalLights[0].timestamp = filter.value?.[0] ?? 0;
}
}

if (!isDaytime(mapState.latitude, mapState.longitude, timestamp)) {
// TODO: interpolate for dusk/dawn
// TODO: Should we avoid mutating the effect? (didn't work when tried defensive copying)
lightShadowEffect.deckEffect.shadowColor[3] = 0;
}
}
return effects.map(effect => effect.deckEffect);
return effects.map(effect => {
updateEffect({visState, mapState, effect});
return effect.deckEffect;
});
}

/**
Expand Down Expand Up @@ -82,3 +70,41 @@ function isDaytime(lat, lon, timestamp) {
const {sunrise, sunset} = SunCalc.getTimes(date, lat, lon);
return date >= sunrise && date <= sunset;
}

/**
* Update effect to match latest vis and map states
*/
function updateEffect({visState, mapState, effect}) {
if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
let {timestamp, timeMode} = effect.config.params;
const sunLight = effect.deckEffect.directionalLights[0];

// set timestamp for shadow
if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.current) {
timestamp = Date.now();
sunLight.timestamp = timestamp;
} else if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.animation) {
timestamp = visState.animationConfig.currentTime ?? 0;
if (!timestamp) {
const filter = visState.filters.find(
filter =>
filter.type === FILTER_TYPES.timeRange &&
(filter.view === FILTER_VIEW_TYPES.enlarged || filter.syncedWithLayerTimeline)
);
if (filter) {
timestamp = filter.value?.[0] ?? 0;
}
}
sunLight.timestamp = timestamp;
}

// output uniform shadow during nighttime
if (isDaytime(mapState.latitude, mapState.longitude, timestamp)) {
effect.deckEffect.outputUniformShadow = false;
sunLight.intensity = effect.config.params.sunLightIntensity;
} else {
effect.deckEffect.outputUniformShadow = true;
sunLight.intensity = 0;
}
}
}

0 comments on commit a36ec68

Please sign in to comment.