Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

HARP-10163: sunny coffee example #1515

Merged
merged 1 commit into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions @here/harp-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"ncp": "^2.0.0",
"stats.js": "^0.17.0",
"style-loader": "^1.0.0",
"suncalc": "^1.8.0",
"three": "^0.115.0",
"ts-loader": "^6.0.3",
"typescript": "^3.9.2",
Expand Down
324 changes: 324 additions & 0 deletions @here/harp-examples/src/real-time-shadows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

import { Theme } from "@here/harp-datasource-protocol";
import { GeoCoordinates } from "@here/harp-geoutils";
import { MapControls, MapControlsUI } from "@here/harp-map-controls";
import { CopyrightElementHandler, MapView, MapViewEventNames } from "@here/harp-mapview";
import { APIFormat, AuthenticationMethod, OmvDataSource } from "@here/harp-omv-datasource";
import { GUI } from "dat.gui";
import * as THREE from "three";
import "three/examples/js/controls/TrackballControls";
import { apikey, copyrightInfo } from "../config";

// tslint:disable-next-line:no-var-requires
const SunCalc = require("suncalc");

const FADE_DURATION = 30 * 60 * 1000; // in ms
const COLOR_CHANGE_DURATION = 2 * FADE_DURATION; // in ms
const TOTAL_FADE_DURATION = FADE_DURATION + COLOR_CHANGE_DURATION;
const COLOR_INTENSITY_FACTOR = 1.5;
const SUNRISE_COLOR = new THREE.Color("hsl(45, 100%, 75%)");
const SUNSET_COLOR = new THREE.Color("hsl(30, 100%, 60%)");

let map: MapView;
let mapControls: MapControls;
let trackball: any;
let debugCamera: THREE.PerspectiveCamera;
let directionalLightHelper: THREE.DirectionalLightHelper;
let shadowCameraHelper: THREE.CameraHelper;
let sun: THREE.DirectionalLight;
let MAX_SUN_INTENSITY: number;
const MAIN_SUN_COLOR = new THREE.Color();

const HERE = new GeoCoordinates(52.530932, 13.3849151);

const date = new Date();
const guiOptions = {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
time: date.getHours() + date.getMinutes() / 60,
timeIndicator: `${date.getHours()}:${date.getMinutes()}`,
debugCamera: false
};
// Reference solar noon time is used to calculate time offsets at specific coordinates.
const refSolarNoon = SunCalc.getTimes(date, 0, 0).solarNoon;
// Main time offset.
const refTime = refSolarNoon.getTime() + date.getTimezoneOffset() * 60 * 1000;

function swapCamera() {
mapControls.enabled = !mapControls.enabled;
trackball.enabled = !trackball.enabled;
map.pointOfView = mapControls.enabled ? undefined : debugCamera;
directionalLightHelper.visible = !directionalLightHelper.visible;
shadowCameraHelper.visible = !shadowCameraHelper.visible;
}

function setupDebugStuff() {
// tslint:disable-next-line: no-string-literal
const mapCameraHelper = new THREE.CameraHelper(map["m_rteCamera"]);
mapCameraHelper.renderOrder = Number.MAX_SAFE_INTEGER;
map.scene.add(mapCameraHelper);

debugCamera = new THREE.PerspectiveCamera(
map.camera.fov,
map.canvas.width / map.canvas.height,
100,
100000
);
map.scene.add(debugCamera);
debugCamera.position.set(6000, 2000, 1000);

trackball = new (THREE as any).TrackballControls(debugCamera, map.canvas);
trackball.enabled = false;
trackball.addEventListener("start", () => {
map.beginAnimation();
});
trackball.addEventListener("end", () => {
map.endAnimation();
});
trackball.addEventListener("change", () => {
map.update();
});

trackball.staticMoving = true;
trackball.rotateSpeed = 3.0;
trackball.zoomSpeed = 4.0;
trackball.panSpeed = 2.0;

directionalLightHelper = new THREE.DirectionalLightHelper(sun, 500);
directionalLightHelper.visible = false;
map.scene.add(directionalLightHelper);

shadowCameraHelper = new THREE.CameraHelper(sun.shadow.camera);
shadowCameraHelper.visible = false;
map.scene.add(shadowCameraHelper);

let lastZoomLevel = map.zoomLevel;
map.addEventListener(MapViewEventNames.Render, () => {
const trackballTarget = trackball.target as THREE.Vector3;
if (lastZoomLevel !== map.zoomLevel) {
trackballTarget.set(0, 0, -map.targetDistance);
lastZoomLevel = map.zoomLevel;
}
trackball.update();

const enableCameraHelpers = map.pointOfView !== undefined;
if (enableCameraHelpers) {
mapCameraHelper.update();
}
mapCameraHelper.visible = enableCameraHelpers;

directionalLightHelper.update();
shadowCameraHelper.update();
});
}

function update() {
guiOptions.time = guiOptions.hours + guiOptions.minutes / 60;
guiOptions.timeIndicator = `${guiOptions.hours}:${guiOptions.minutes}`;

const { latitude, longitude } = map.geoCenter;
const lightPos = sun.position;
// Dirty time is a time without taking into account the time offset at the specific coordinates.
const dirtyTime = new Date(
guiOptions.year,
guiOptions.month - 1,
guiOptions.day,
guiOptions.hours,
guiOptions.minutes,
0
);

// Calculating time offset at current location.
const timeOffset = SunCalc.getTimes(date, latitude, longitude).solarNoon.getTime() - refTime;
// Time with corrected offset.
const locationDate = new Date(dirtyTime.getTime() + timeOffset);

const sunTimes = SunCalc.getTimes(locationDate, latitude, longitude);
const sunPosition = SunCalc.getPosition(locationDate, latitude, longitude);

const azimuth = sunPosition.azimuth;
const altitude = sunPosition.altitude - Math.PI / 2;

const r = map.targetDistance;
lightPos.setX(r * Math.sin(altitude) * Math.sin(azimuth));
lightPos.setY(r * Math.sin(altitude) * Math.cos(azimuth));
lightPos.setZ(r * Math.cos(altitude) - r);
// Resetting the target is important, because this is overriden in the MapView.
// This is an ugly hack and HARP-10353 should improve this.
sun.target.position.set(0, 0, -r);

sun.color.set(MAIN_SUN_COLOR);

const location_ms = locationDate.getTime();
const sunriseDiff = location_ms - sunTimes.sunriseEnd.getTime();
const sunsetDiff = sunTimes.sunsetStart.getTime() - location_ms;
if (sunriseDiff > 0 && sunsetDiff > 0) {
if (sunriseDiff < TOTAL_FADE_DURATION || sunsetDiff < TOTAL_FADE_DURATION) {
let color: THREE.Color;
let colorDiff: number;
if (azimuth < 0) {
color = SUNRISE_COLOR;
colorDiff = sunriseDiff;
} else {
color = SUNSET_COLOR;
colorDiff = sunsetDiff;
}
sun.color.lerpHSL(
color,
THREE.MathUtils.clamp(1 - (colorDiff - FADE_DURATION) / COLOR_CHANGE_DURATION, 0, 1)
);

if (colorDiff <= FADE_DURATION) {
sun.intensity = THREE.MathUtils.lerp(
0,
MAX_SUN_INTENSITY * COLOR_INTENSITY_FACTOR,
colorDiff / FADE_DURATION
);
} else {
sun.intensity = THREE.MathUtils.lerp(
MAX_SUN_INTENSITY,
MAX_SUN_INTENSITY * COLOR_INTENSITY_FACTOR,
THREE.MathUtils.clamp(
1 - (colorDiff - FADE_DURATION) / COLOR_CHANGE_DURATION,
0,
1
)
);
}
} else {
sun.intensity = MAX_SUN_INTENSITY;
}
} else {
sun.intensity = 0;
}

map.update();
}

function initializeMapView(id: string, theme: Theme): MapView {
const canvas = document.getElementById(id) as HTMLCanvasElement;
map = new MapView({
canvas,
theme,
enableShadows: true
});
map.renderLabels = false;
map.fog.enabled = false;

CopyrightElementHandler.install("copyrightNotice", map);

mapControls = new MapControls(map);
mapControls.maxTiltAngle = 50;

const ui = new MapControlsUI(mapControls);
canvas.parentElement!.appendChild(ui.domElement);

map.lookAt({ target: HERE, zoomLevel: 17 });

map.resize(window.innerWidth, window.innerHeight);
window.addEventListener("resize", () => {
map.resize(window.innerWidth, window.innerHeight);
});

addOmvDataSource().then(() => {
const light = map.lights.find(item => item instanceof THREE.DirectionalLight) as
| THREE.DirectionalLight
| undefined;
if (light === undefined) {
throw new Error("Light for a sun was not found.");
}
sun = light;
MAX_SUN_INTENSITY = sun.intensity;
MAIN_SUN_COLOR.copy(sun.color);

map.addEventListener(MapViewEventNames.MovementFinished, update);

addGuiElements();
setupDebugStuff();
update();
});

return map;
}

const addOmvDataSource = (): Promise<void> => {
const omvDataSource = new OmvDataSource({
baseUrl: "https://vector.hereapi.com/v2/vectortiles/base/mc",
apiFormat: APIFormat.XYZOMV,
styleSetName: "tilezen",
authenticationCode: apikey,
authenticationMethod: {
method: AuthenticationMethod.QueryString,
name: "apikey"
},
copyrightInfo
});

return map.addDataSource(omvDataSource);
};

function addGuiElements() {
// Control light direction
const gui = new GUI({ width: 300 });
gui.add(guiOptions, "year").onChange(update);
gui.add(guiOptions, "month").onChange(update);
gui.add(guiOptions, "day").onChange(update);
const timeSlider = gui.add(guiOptions, "time", 0, 24, 0.01);
const timeIndicator = gui.add(guiOptions, "timeIndicator");
timeSlider.onChange(() => {
guiOptions.hours = Math.floor(guiOptions.time);
guiOptions.minutes = Math.floor((guiOptions.time - guiOptions.hours) * 60);

update();
timeIndicator.updateDisplay();
});
timeIndicator.onChange(() => {
const time = guiOptions.timeIndicator.split(":");
guiOptions.hours = parseInt(time[0], 10);
guiOptions.minutes = parseInt(time[1], 10);

update();
timeSlider.updateDisplay();
});
gui.add(guiOptions, "debugCamera").onChange(swapCamera);
}

export namespace RealTimeShadows {
const theme: Theme = {
extends: "resources/berlin_tilezen_base.json",
lights: [
{
type: "ambient",
color: "#ffffff",
name: "ambientLight",
intensity: 0.9
},
{
type: "directional",
color: "#ffffff",
name: "light1",
intensity: 1,
// Will be overriden immediately, see `update`
direction: {
x: 0,
y: 0.01,
z: -1
},
castShadow: true
}
],
definitions: {
// Opaque buildings
defaultBuildingColor: "#EDE7E1FF"
}
};
initializeMapView("mapCanvas", theme);
}
Loading