Experience results from numerical simulations by the CEREMA in 2 locations in France. Build on OpenStreetMap, IGN data and AFrame for WebXR support.
https://bric-vr.com/cerema/portail/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
722 lines
30 KiB
722 lines
30 KiB
2 years ago
|
<html>
|
||
|
<head>
|
||
|
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
||
|
<script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/instanced-mesh@v0.6.0/src/instanced-mesh.min.js"></script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<script>
|
||
|
if(window.location.protocol != 'https:') { location.href = location.href.replace("http://", "https://"); }
|
||
|
|
||
|
var simsrc = "simulations/initial_simulation_data/crop_outpy_"
|
||
|
var forceSimulationSource = AFRAME.utils.getUrlParameter('simsrc');
|
||
|
if ( forceSimulationSource ) {
|
||
|
simsrc = forceSimulationSource
|
||
|
console.log(simsrc,"switched from default simulation to this one")
|
||
|
// sould also update the src of id="debug_sim_cerema" with a specific step, e.g 50
|
||
|
}
|
||
|
var simscale = 0.035
|
||
|
var forceDisplacementScale = AFRAME.utils.getUrlParameter('simscale');
|
||
|
if ( forceDisplacementScale ) {
|
||
|
simscale = forceDisplacementScale
|
||
|
console.log(simsrc,"switched from default simulation scale to this one", simscale)
|
||
|
}
|
||
|
const vrHeight = 0.8
|
||
|
const desktopHeight = 2.5
|
||
|
const simext = ".jpg"
|
||
|
const simstart = 1
|
||
|
const simend = 95 // mostly correct as new simulations got to 96 steps
|
||
|
const sim_filename_padding = true
|
||
|
const animationSpeedStep = 900
|
||
|
var animationCurrentStep = simstart
|
||
|
|
||
|
var wsimanim
|
||
|
|
||
|
function animationStep(){
|
||
|
wsim.setAttribute("material", {
|
||
|
// offset: {x: animationCurrentStep/1000, y:0}, // impacts also the displacement map, not just the texture
|
||
|
displacementMap: simsrc+(animationCurrentStep++)+simext,
|
||
|
})
|
||
|
//wsim.setAttribute("material", "displacementMap", simsrc+(animationCurrentStep++)+simext)
|
||
|
//wsim.object3D.children[0].material.needsUpdate = true //no need to do this for every step...
|
||
|
//wsim.object3D.children[0].material.map.offset.setX(animationCurrentStep/1000) // same problem, that offset is not just for the texture but also for the displacement map
|
||
|
// should find a way to decouple
|
||
|
// otherwise could try a custom shader but might still encounter the same problem
|
||
|
if (animationCurrentStep >= simend) resetAnimations()
|
||
|
}
|
||
|
|
||
|
var currentGeoPose = null; // https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition
|
||
|
// navigator.geolocation.getCurrentPosition( geolocalized ) // should handle error, including being ignored
|
||
|
// disable for faster testing
|
||
|
|
||
|
// to consider https://github.com/mourner/suncalc
|
||
|
|
||
|
// otherwise have 2 fallbacks via menu
|
||
|
var positionsCurated = [
|
||
|
{ position:[50.8365354, 4.3581719], hotspots: []},
|
||
|
{ shortname: "baquets", worldposition:[-38, 17], position:[49.3516400, 0.5461600], // way heavier!
|
||
|
offsetLandscapePosition: "-124.32254 0.78377 -46.41836", // TODO use, might prefer something easier to compute to add e.g Vector3, or do pose (position/rotation/scale) per id
|
||
|
// alternatively might show/hide per classname based on shortname
|
||
|
offsetWatersimPosition: "-124.32254 0.78377 -46.41836", // TODO use
|
||
|
hotspots: [
|
||
|
// done via the AFrame inspector then pasting here, then regex
|
||
|
"-36.50124 3 18.52317", "-15.31684 3 26.46878", "-42.59553 3 32.46203", "12.26538 3 38.90027", "-0.69236 3 67.76877", "-49.01323 3 48.21272", "-79.56723 3 34.81311", "-94.0 3 25.0", "-19.2 3 60", "-86.58562 3 -5.73462", "-58.70357 3 8.36794",
|
||
|
],
|
||
|
visualMarkerPositions:[
|
||
|
// {rotation:"0 -90 0" , position:"54.31842 -0.21946 -335.17472"},
|
||
|
{rotation:"0 -22.49241 0", position:"-30.15801 0.05 14.66674"},
|
||
|
{rotation:"0 66.80401412327834 0" , position:"-41.94021 0.05 19.43"}
|
||
|
],
|
||
|
},
|
||
|
{ shortname: "ruelle", worldposition:[-126, -40], position:[49.3605700, 0.5031000],
|
||
|
offsetLandscapePosition: "-124.32254 0.78377 -46.41836", // TODO use
|
||
|
offsetWatersimPosition: "-124.32254 0.78377 -46.41836", // TODO use
|
||
|
hotspots: [ "-134.62038 3 -13.29107", "-125.89544 3 -30.36393", "-119.72245 3 -49.78609", "-96.91944 3 -95.00968", "-90.17903 3 -109.1628", "-142.76086 3 11.1124"],
|
||
|
visualMarkerPositions:[
|
||
|
{position:"-125.67578 0.78377 -46.72093"},
|
||
|
{position:"-132.39466 0.78377 -30.06373"},
|
||
|
{position:"-138.95986 0.78377 -13.43454"},
|
||
|
],
|
||
|
},
|
||
|
]
|
||
|
|
||
|
var textureOffsetStart = 0
|
||
|
var textureOffsetEnd = 1
|
||
|
AFRAME.registerComponent('texture-drift', {
|
||
|
// should be able to animate instead... but somehow didn't manage.
|
||
|
// animation_texture="property: components.material.material.map.offset.x; from: 0; to: 1; dur: 2000; easing: linear; loop: true; dir: alternate;"
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
el.addEventListener('materialtextureloaded', function () {
|
||
|
console.log(el,"text loaded")
|
||
|
setTimeout(function () { // setTimeout for good measure.
|
||
|
var x = el.components.material.material.map.offset.x
|
||
|
setInterval( _ => {
|
||
|
(x >= textureOffsetEnd) ? x = textureOffsetStart : x += .0001
|
||
|
}, 100)
|
||
|
}, 200);
|
||
|
});
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('randomfx', {
|
||
|
init: function () {
|
||
|
var FXsounds = Array.from( document.querySelectorAll(".randomfx") )
|
||
|
setInterval( _ => { FXsounds[Math.floor(Math.random()*FXsounds.length)].components.sound.playSound() }, 9000 )
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('set-initial-camera-from-data', {
|
||
|
init: function () {
|
||
|
this.el.setAttribute("position", positionsCurated[selection].worldposition[0] + " "+vrHeight+" " + positionsCurated[selection].worldposition[1] )
|
||
|
}
|
||
|
})
|
||
|
var ignoreCache = true // should be joined as 1 or better be location dependent
|
||
|
const disableWaysForGLTFExportInHubs = false
|
||
|
|
||
|
var selection = 1 // default position fallback to faciliate tests
|
||
|
|
||
|
// initially done as a different page at first with query parameter
|
||
|
var forceSelection = AFRAME.utils.getUrlParameter('position');
|
||
|
if ( forceSelection ) selection = forceSelection
|
||
|
|
||
|
var position = positionsCurated[selection].position
|
||
|
var hotspots = positionsCurated[selection].hotspots
|
||
|
var visualmarkers = positionsCurated[selection].visualMarkerPositions
|
||
|
|
||
|
var precision = 4
|
||
|
var bbox_limit = 10/10**precision
|
||
|
bbox_limit = 3*10/10**precision
|
||
|
|
||
|
var elevation_res = 10
|
||
|
|
||
|
// generate 3D model from the target location from OSM data
|
||
|
var bbox = "" + (position[0]-bbox_limit/2)
|
||
|
+ "," + (position[1]-bbox_limit/2)
|
||
|
+ "," + (position[0]+bbox_limit-bbox_limit/2)
|
||
|
+ "," + (position[1]+bbox_limit-bbox_limit/2)
|
||
|
|
||
|
var queryOSMgeom = `https://overpass-api.de/api/interpreter?data=[out:json];nwr(${bbox});out geom;`
|
||
|
|
||
|
// fetch flood data of target location (should be only 2 available) as timeseries
|
||
|
|
||
|
var locations = []
|
||
|
for (var i=0;i<elevation_res;i++)
|
||
|
for (var j=0;j<elevation_res;j++)
|
||
|
locations.push({
|
||
|
latitude:Number( (position[0]+(bbox_limit/elevation_res)*i).toFixed(precision) ),
|
||
|
longitude:Number( (position[1]+(bbox_limit/elevation_res)*j).toFixed(precision) ),
|
||
|
})
|
||
|
|
||
|
var queryElevation = 'https://api.open-elevation.com/api/v1/lookup'
|
||
|
// doesn't seem to have sufficient resolution, might use IGN instead
|
||
|
|
||
|
var buildingHeight = 6
|
||
|
var forceHeight = AFRAME.utils.getUrlParameter('height');
|
||
|
if ( forceHeight ) buildingHeight = Number( forceHeight )
|
||
|
var wayHeight = .5
|
||
|
|
||
|
var globaljosm
|
||
|
var offsets = { // used to re-center the map
|
||
|
"x": Number( position[0].toFixed(precision-1) ),
|
||
|
"y": 0.1, // try to avoid z-fighting
|
||
|
"z": Number( position[1].toFixed(precision-1) )}
|
||
|
var scale = 10**(precision+1)
|
||
|
|
||
|
function pointFromCoordinates( p ){
|
||
|
return new THREE.Vector3( (p.lat-offsets.x)*scale, 0, (p.lon-offsets.z)*scale )
|
||
|
}
|
||
|
|
||
|
function curvesTo3DElements( curves, el ){
|
||
|
curves.map( c => {
|
||
|
makeLine( c.geometry.coordinates.map( p => pointFromCoordinates({lat:p[1],lon:p[0]}) ) , c.properties.ALTITUDE, el )
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function elevationTo3DElements( elevations, el ){
|
||
|
localStorage.setItem('elevation', JSON.stringify( elevations ) )
|
||
|
var minElevation = elevations.sort( (a,b) => a.elevation - b.elevation )[0].elevation
|
||
|
elevations.map( h => {
|
||
|
var pointEl = document.createElement("a-sphere")
|
||
|
pointEl.setAttribute("color", "blue")
|
||
|
pointEl.setAttribute("radius", "0.1")
|
||
|
// simplification, for a proper polygon see make3DFloor() but here shouldn't assume a centroid
|
||
|
// could instead rpointEly on a grid
|
||
|
var pos = pointFromCoordinates( {lat:h.latitude,lon:h.longitude} )
|
||
|
pos.y = h.elevation - minElevation
|
||
|
pointEl.setAttribute("position", AFRAME.utils.coordinates.stringify( pos ) )
|
||
|
// see pointFromCoordinates
|
||
|
// could find the minimum altitude then remove it from every other values
|
||
|
el.appendChild( pointEl )
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function fromOSMTo3DElements( josm, el ){
|
||
|
localStorage.setItem('osm', JSON.stringify( josm ) )
|
||
|
globaljosm = josm // used for debug in console
|
||
|
|
||
|
// right click on elements on https://overpass-turbo.eu/s/1jbc allows to see type visually
|
||
|
//var houses = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["add:housenumber"]) ).map( e => e.geometry)
|
||
|
var houses = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["building"]) )
|
||
|
.filter( i => !loadedosmways.includes(i.id) )
|
||
|
.filter( i => (i.id < 561974951 || i.id > 561974997) ) // does not seem present in the model (assuming sequential OSM list, as of now)
|
||
|
.map( e => e.geometry)
|
||
|
houses.map( e => { makeBuilding( e, buildingHeight*(1+Math.random()), el ) })
|
||
|
idfound = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["building"]) )
|
||
|
idfoundfilter = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["building"]) )
|
||
|
.filter( i => !loadedosmways.includes(i.id) )
|
||
|
console.log("way IDs found", idfound)
|
||
|
console.log("way IDs filtered", idfoundfilter)
|
||
|
|
||
|
// to add trees
|
||
|
var trees = josm.elements.filter( i => i.type == "node" && i.tags && i.tags.natural == "tree")
|
||
|
var lamp = josm.elements.filter( i => i.type == "node" && i.tags && i.tags.highway == "street_lamp")
|
||
|
// https://www.openstreetmap.org/node/9923232483
|
||
|
|
||
|
trees.map( e => { makeTree( e, el ) } )
|
||
|
lamp.map( e => { makeLamp( e, el ) } )
|
||
|
|
||
|
/* consider also
|
||
|
landuse forest e.g https://www.openstreetmap.org/way/42198660
|
||
|
waterway river e.g https://www.openstreetmap.org/way/112050282
|
||
|
or rather with type multipolygon https://www.openstreetmap.org/relation/8029633
|
||
|
*/
|
||
|
|
||
|
// console.log('comparing ways from OSM to glTF')
|
||
|
// existing_houses = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["building"]) )
|
||
|
// .filter( i => loadedosmways.includes(i.id) )
|
||
|
// .map( e => e.geometry)
|
||
|
// existing_houses.map( e => { makeBuilding( e, buildingHeight*(.5), el ) })
|
||
|
// used for alignment
|
||
|
|
||
|
//var highways = josm.elements.filter( e => (e.type == "way" && e.tags["highway"]) ).map( e => e.geometry)
|
||
|
var highways = josm.elements.filter( e => (e.type == "way" && e.tags && e.tags["highway"]) ).map( e => e.geometry)
|
||
|
// not so good, shows paths rather than a usable geometry
|
||
|
if (!disableWaysForGLTFExportInHubs)
|
||
|
highways.map( e => { makeWay( e, wayHeight, el, 0x120a06 ) })
|
||
|
}
|
||
|
|
||
|
function geolocalized( pos ){
|
||
|
console.log( pos.coords )
|
||
|
}
|
||
|
|
||
|
const loader = new THREE.TextureLoader();
|
||
|
|
||
|
function addWall(a, b, h, el, color=0x6d6a64){
|
||
|
var geometry = new THREE.BufferGeometry();
|
||
|
// create a simple rectangle shape. We duplicate the top left and bottom right
|
||
|
// vertices because each vertex needs to appear once per triangle.
|
||
|
var vertices = new Float32Array( [
|
||
|
a.x, a.y, a.z,
|
||
|
a.x, a.y+h, a.z,
|
||
|
b.x, b.y, b.z,
|
||
|
|
||
|
a.x, a.y+h, a.z,
|
||
|
b.x, b.y+h, b.z,
|
||
|
b.x, b.y, b.z,
|
||
|
] );
|
||
|
|
||
|
// itemSize = 3 because there are 3 values (components) per vertex
|
||
|
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
|
||
|
const material = new THREE.MeshBasicMaterial({
|
||
|
color : new THREE.Color( 0x6d6a64 ).offsetHSL(0,0,-Math.random()/10),
|
||
|
side: THREE.DoubleSide,
|
||
|
//map: loader.load('Visuals/TEXTURE_BAT/TEXTURE_BAT_Base_Color.jpg'),
|
||
|
});
|
||
|
|
||
|
var mesh = new THREE.Mesh( geometry, material );
|
||
|
var entity = document.createElement("a-entity");
|
||
|
// not sure it's the best level of abstraction, maybe building instead
|
||
|
entity.object3D.add( mesh );
|
||
|
el.appendChild( entity );
|
||
|
|
||
|
}
|
||
|
|
||
|
function addFan(a, b, c, el, verticalOffset, color ){
|
||
|
var geometry = new THREE.BufferGeometry();
|
||
|
var vertices = new Float32Array( [
|
||
|
a.x, a.y+verticalOffset, a.z,
|
||
|
b.x, b.y+verticalOffset, b.z,
|
||
|
c.x, c.y+verticalOffset, c.z,
|
||
|
] );
|
||
|
|
||
|
// itemSize = 3 because there are 3 values (components) per vertex
|
||
|
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
|
||
|
const material = new THREE.MeshBasicMaterial({
|
||
|
color : color,
|
||
|
side: THREE.DoubleSide,
|
||
|
});
|
||
|
var mesh = new THREE.Mesh( geometry, material );
|
||
|
var entity = document.createElement("a-entity");
|
||
|
entity.object3D.add( mesh );
|
||
|
el.appendChild( entity );
|
||
|
}
|
||
|
|
||
|
function make3DFloor(points, el, color=0xedeceb ){
|
||
|
var entity = document.createElement("a-entity")
|
||
|
var centroid = getCentroid( points )
|
||
|
entity.id = "roomcentroid"
|
||
|
entity.setAttribute("position", centroid)
|
||
|
el.appendChild( entity )
|
||
|
|
||
|
points.map( (e,i,arr) => {
|
||
|
(i==arr.length-1) ? addFan(arr[0], e, centroid, el, 0, color) : addFan(e, arr[i+1], centroid, el, 0, color)
|
||
|
})
|
||
|
|
||
|
}
|
||
|
|
||
|
function make3DRoof(points, el , height, color=0x29465b){
|
||
|
var entity = document.createElement("a-entity")
|
||
|
var centroid = getCentroid( points )
|
||
|
centroid.y += 3 // pointy roof
|
||
|
entity.id = "roomcentroid"
|
||
|
entity.setAttribute("position", centroid)
|
||
|
el.appendChild( entity )
|
||
|
|
||
|
points.map( (e,i,arr) => {
|
||
|
(i==arr.length-1) ? addFan(arr[0], e, centroid, el, height, color) : addFan(e, arr[i+1], centroid, el, height, color)
|
||
|
})
|
||
|
|
||
|
}
|
||
|
|
||
|
function getCentroid(points){
|
||
|
var centroid = new THREE.Vector3(0,0,0);
|
||
|
// can probably be simplied now that we rely on threejs vectors rather than AFrame elements
|
||
|
for (var e of points) {
|
||
|
centroid.add( e );
|
||
|
}
|
||
|
centroid.divideScalar(points.length);
|
||
|
return centroid;
|
||
|
}
|
||
|
|
||
|
function makeWay( points, height, el, color=0xCCCCCC ){
|
||
|
var poly = points.map( p => pointFromCoordinates( p ) )
|
||
|
var way = document.createElement("a-entity")
|
||
|
//way.setAttribute("position", "0 1 0") // vertical offset to avoid z-fighting on large horizontal planes
|
||
|
way.className = "way"
|
||
|
way.setAttribute("shadow", "") // somehow doesn't seem to receive, only casts
|
||
|
el.appendChild( way )
|
||
|
make3DFloor( poly, way, color, "Visuals/TEXTURE_BAT/TEXTURE_SOL_Base_Color.jpg")
|
||
|
}
|
||
|
|
||
|
function makeLamp( treedata, el){
|
||
|
var tree = document.createElement("a-entity")
|
||
|
tree.setAttribute("position", AFRAME.utils.coordinates.stringify(
|
||
|
pointFromCoordinates( {lat:treedata.lat, lon:treedata.lon} )
|
||
|
) )
|
||
|
tree.setAttribute("instanced-mesh-member", "mesh:#meshlamp")
|
||
|
el.appendChild( tree )
|
||
|
}
|
||
|
|
||
|
function makeTree( treedata, el){
|
||
|
var tree = document.createElement("a-entity")
|
||
|
tree.setAttribute("position", AFRAME.utils.coordinates.stringify(
|
||
|
pointFromCoordinates( {lat:treedata.lat, lon:treedata.lon} )
|
||
|
) )
|
||
|
//tree.setAttribute("rotation", "0 "+ Math.random()*360 +" 0") // changing scale ever so slightly non uniformely could be interesting too
|
||
|
// double check documentation, seems to move them too
|
||
|
tree.setAttribute("instanced-mesh-member", "mesh:#meshtree")
|
||
|
el.appendChild( tree )
|
||
|
}
|
||
|
|
||
|
function makeBuilding( points, height, el ){
|
||
|
// somehow there is an angle, a slanting. Building usually have right angles at corners, not here.
|
||
|
var poly = points.map( p => pointFromCoordinates( p ) )
|
||
|
var building = document.createElement("a-entity")
|
||
|
building.className = "building"
|
||
|
building.setAttribute("position", "0 "+ Math.random()/10 + " 0") // could rely on elevation instead.
|
||
|
//Pavement would need to be high enough to make the connection between that and street.
|
||
|
//which might make it look pointless
|
||
|
el.appendChild( building )
|
||
|
makeWalls( poly, height, building )
|
||
|
// make3DFloor( poly, building ) // not visibile so might be removed
|
||
|
var color = new THREE.Color( 0x262D7B ).offsetHSL(0,0,-Math.random());
|
||
|
make3DRoof( poly, building, height, color )
|
||
|
}
|
||
|
|
||
|
function makeLine( poly, h, el ){
|
||
|
var color = 0x6d6a64 //+Math.floor( 100*Math.random() )
|
||
|
poly.map( (e,i,arr) => { if (i<arr.length-1) addWall(e, arr[i+1], h, el, color) })
|
||
|
}
|
||
|
|
||
|
function makeWalls( poly, h, el ){
|
||
|
var color = new THREE.Color( 0x808080 ).offsetHSL(0,0,-Math.random()/10);
|
||
|
poly.map( (e,i,arr) => { (i==arr.length-1) ? addWall(arr[0], e, h, el, color) : addWall(e, arr[i+1], h, el, color) })
|
||
|
}
|
||
|
|
||
|
var elevationDataFromCache = localStorage.getItem('elevation');
|
||
|
// includes request date so might prefer to invalidate after a while
|
||
|
var osmDataFromCache = localStorage.getItem('osm');
|
||
|
|
||
|
if (ignoreCache){
|
||
|
elevationDataFromCache = null
|
||
|
osmDataFromCache = null
|
||
|
}
|
||
|
|
||
|
AFRAME.registerComponent('elevation', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
if (!elevationDataFromCache){
|
||
|
console.log("no elevation data from cache, querying")
|
||
|
|
||
|
fetch(queryElevation, {
|
||
|
method: 'POST',
|
||
|
headers: {
|
||
|
'Accept': 'application/json',
|
||
|
'Content-Type': 'application/json',
|
||
|
},
|
||
|
body: JSON.stringify({locations:locations})
|
||
|
}).then(response => response.json()).then(data => elevationTo3DElements(data.results, el ) )
|
||
|
} else {
|
||
|
console.log("elevation data from cache")
|
||
|
elevationTo3DElements( JSON.parse( elevationDataFromCache ), el )
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('osm', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
if (!osmDataFromCache){
|
||
|
console.log("no osm data from cache, querying")
|
||
|
fetch(queryOSMgeom).then(response => response.json()).then(data => fromOSMTo3DElements( data, el ) )
|
||
|
//console.log("no osm data from cache, querying locally for demo")
|
||
|
//fetch("cached_osm_request.json").then(response => response.json()).then(data => fromOSMTo3DElements( data, el ) )
|
||
|
// for demo safety, cached locally on 6/1/2023 as cached_osm_request.json
|
||
|
} else {
|
||
|
console.log("osm data from cache")
|
||
|
fromOSMTo3DElements( JSON.parse( osmDataFromCache ), el )
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
|
||
|
// includes request date so might prefer to invalidate after a while
|
||
|
|
||
|
AFRAME.registerComponent('cerema', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
wsim = el
|
||
|
animationCurrentStep = simstart
|
||
|
wsimanim = setInterval(animationStep, animationSpeedStep)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('watervisuals', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
visualmarkers.map( h => {
|
||
|
var pointEl = document.createElement("a-gltf-model")
|
||
|
pointEl.setAttribute("gltf-model", "Visuals/ASSETS_3D/AA_Crue.glb")
|
||
|
pointEl.setAttribute("scale", "60 60 60")
|
||
|
pointEl.setAttribute("rotation", "0 70 0")
|
||
|
pointEl.setAttribute("position", h.position)
|
||
|
if (h.rotation) pointEl.setAttribute("rotation", h.rotation)
|
||
|
el.appendChild( pointEl )
|
||
|
})
|
||
|
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('hotspots', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
hotspots.map( h => {
|
||
|
var pointEl = document.createElement("a-gltf-model")
|
||
|
pointEl.setAttribute("gltf-model", "Visuals/ASSETS_3D/AA_Hotspot.glb")
|
||
|
pointEl.setAttribute("model-opacity", .3)
|
||
|
pointEl.setAttribute("scale", "50 50 50")
|
||
|
pointEl.setAttribute("teleport-via-cursor", true)
|
||
|
pointEl.setAttribute("position", h)
|
||
|
el.appendChild( pointEl )
|
||
|
})
|
||
|
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// from https://stackoverflow.com/questions/43914818/alpha-animation-in-aframe-for-a-object-model
|
||
|
AFRAME.registerComponent('model-opacity', {
|
||
|
schema: {default: 1.0},
|
||
|
init: function () {
|
||
|
this.el.addEventListener('model-loaded', this.update.bind(this));
|
||
|
},
|
||
|
update: function () {
|
||
|
var mesh = this.el.getObject3D('mesh');
|
||
|
var data = this.data;
|
||
|
if (!mesh) { return; }
|
||
|
mesh.traverse(function (node) {
|
||
|
if (node.isMesh) {
|
||
|
node.material.opacity = data;
|
||
|
node.material.transparent = data < 1.0;
|
||
|
node.material.needsUpdate = true;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
AFRAME.registerComponent('teleport-via-cursor', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
var cam = document.querySelector("#camera-rig")
|
||
|
this.el.addEventListener('click', function (evt) {
|
||
|
var pos = el.getAttribute("position")
|
||
|
if (!AFRAME.utils.device.checkHeadsetConnected())
|
||
|
cam.setAttribute("position", pos.x + " " + desktopHeight + " " + pos.z)
|
||
|
else
|
||
|
cam.setAttribute("position", pos.x + " " + vrHeight + " " + pos.z)
|
||
|
// might want to blink too, e.g black sphere around the camera for .5s
|
||
|
});
|
||
|
this.el.addEventListener('fusing', function (evt) {
|
||
|
//el.setAttribute("opacity", 1)
|
||
|
el.setAttribute("model-opacity", 1)
|
||
|
});
|
||
|
this.el.addEventListener('mouseleave', function (evt) {
|
||
|
//el.setAttribute("opacity", 0.3)
|
||
|
el.setAttribute("model-opacity", .3)
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
AFRAME.registerComponent('hud', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
var ringEl = document.createElement("a-ring")
|
||
|
ringEl.setAttribute("theta-length", "0")
|
||
|
ringEl.setAttribute("position", "1.5 0 -3")
|
||
|
el.appendChild( ringEl )
|
||
|
var pointEl = document.createElement("a-text")
|
||
|
// consider a-text instead https://github.com/lojjic/aframe-troika-text
|
||
|
// CEREMA/Visuals/ASSETS_3D/TypoTimer/Abel/Abel-Regular.ttf
|
||
|
pointEl.setAttribute("value", "0")
|
||
|
pointEl.setAttribute("color", "white")
|
||
|
pointEl.setAttribute("negate", "false")
|
||
|
pointEl.setAttribute("text", "align", "center")
|
||
|
pointEl.setAttribute("text", "font", "Visuals/Abel-Regular-msdf.json")
|
||
|
pointEl.setAttribute("position", "0.4 0 -0.9")
|
||
|
el.appendChild( pointEl )
|
||
|
var backgroundModelEl = document.createElement("a-gltf-model")
|
||
|
backgroundModelEl.setAttribute("scale", "40 50 50")
|
||
|
backgroundModelEl.setAttribute("position", "0.5 -0.4 -1.1")
|
||
|
backgroundModelEl.setAttribute("gltf-model", "Visuals/ASSETS_3D/AA_Timer.glb")
|
||
|
el.appendChild( backgroundModelEl )
|
||
|
this.ring = ringEl
|
||
|
this.hud = pointEl
|
||
|
},
|
||
|
|
||
|
tick: function (time, timeDelta) {
|
||
|
animationTime++
|
||
|
if (animationTime > 20000) {
|
||
|
this.start = 0
|
||
|
animationTime = 0
|
||
|
}
|
||
|
this.hud.setAttribute("value", animationCurrentStep)
|
||
|
this.ring.setAttribute("theta-length", animationCurrentStep/simend*360)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('listosmways', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
this.el.addEventListener('model-loaded', function (evt) {
|
||
|
//el.object3D.traverse( i => { if (i.name && i.name !="Scene" && i.type=="Group") loadedosmways.push(Number( i.name )) })
|
||
|
el.object3D.traverse( i => { if (i.name && i.name !="Scene" && i.type=="Mesh") loadedosmways.push(Number( i.name )) })
|
||
|
});
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('resetanimations', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
this.el.addEventListener('click', function (evt) {
|
||
|
resetAnimations()
|
||
|
});
|
||
|
this.el.addEventListener('fusing', function (evt) {
|
||
|
el.setAttribute("opacity", 0.3)
|
||
|
});
|
||
|
this.el.addEventListener('mouseleave', function (evt) {
|
||
|
el.setAttribute("opacity", 1)
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
AFRAME.registerComponent('displaymodels', {
|
||
|
init: function () {
|
||
|
// sufficient, removed the others otherwise causes raycaster bugs
|
||
|
Array.from( document.querySelectorAll('[class*="models_"]') ).map( m => {
|
||
|
m.setAttribute( "scale", ".01 .01 .01" )
|
||
|
})
|
||
|
Array.from( document.querySelectorAll(".models_"+positionsCurated[selection].shortname) ).map( m => {
|
||
|
m.setAttribute( "scale", "1.2 1.2 1.2")
|
||
|
m.setAttribute( "visible", "true" )
|
||
|
m.setAttribute( "listosmways", "" )
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('forcestats', {
|
||
|
init: function () {
|
||
|
var forcestats = AFRAME.utils.getUrlParameter('forcestats');
|
||
|
if (forcestats == "true") this.el.setAttribute("stats", true)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
AFRAME.registerComponent('customdebug', {
|
||
|
init: function () {
|
||
|
var el = this.el
|
||
|
var debugmode = AFRAME.utils.getUrlParameter('debugmode');
|
||
|
if (debugmode && debugmode == "true") {
|
||
|
Array.from( document.querySelectorAll(".debug") ).map( e => e.setAttribute("visible", true) )
|
||
|
document.querySelector("#mainbutton").style.display = "none";
|
||
|
el.addEventListener('loaded', function () {
|
||
|
el.components.inspector.openInspector()
|
||
|
setTimeout(function () { // setTimeout for good measure.
|
||
|
AFRAME.scenes[0].camera.position.set(400,600,-30)
|
||
|
AFRAME.scenes[0].camera.rotation.set(-1.6,0.65,1.6);
|
||
|
}, 1000)
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
function resetAnimations(){
|
||
|
Array.from( document.querySelectorAll("[animation]") )
|
||
|
.map( a => Object.keys( a.components )
|
||
|
.filter( k => k.match("animation") )
|
||
|
.map( key => a.components[key].time = 0 )
|
||
|
// could also use play() or pause()
|
||
|
)
|
||
|
animationTime = 0
|
||
|
animationCurrentStep = simstart
|
||
|
clearInterval(wsimanim)
|
||
|
wsimanim = setInterval(animationStep, animationSpeedStep)
|
||
|
}
|
||
|
|
||
|
var animationTime = 0 // this isn't exactly aligned with animations
|
||
|
// because virtual and real time hasn't be decided yet (waiting on simulation results)
|
||
|
// virtual time would amount to 2 to 3min total.
|
||
|
|
||
|
loadedosmways = [];
|
||
|
|
||
|
function playSound(){
|
||
|
var entity = document.querySelector('[sound]');
|
||
|
entity.components.sound.playSound();
|
||
|
document.querySelector("a-entity[sound]").components.sound.playSound()
|
||
|
document.querySelector("#mainbutton").style.display = "none";
|
||
|
}
|
||
|
|
||
|
function startExperience(){
|
||
|
document.querySelector("#watersim").setAttribute("material", "displacementScale", simscale)
|
||
|
AFRAME.scenes[0].enterVR();
|
||
|
playSound();
|
||
|
resetAnimations()
|
||
|
if (!AFRAME.utils.device.checkHeadsetConnected()){
|
||
|
document.querySelector("#camera-rig").object3D.position.y = desktopHeight
|
||
|
// works only at the beginning, not after teleporting
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
<button id=mainbutton style="z-index: 1; position: absolute; width:50%; margin: auto; text-align:center; top:45%; left:30%; height:30%;" onclick="startExperience()">Demarrer l'experience</button>
|
||
|
<a-scene displaymodels forcestats customdebug >
|
||
|
<a-entity id="hotspots" hotspots ></a-entity>
|
||
|
<a-entity id="watervisuals" watervisuals ></a-entity>
|
||
|
<!-- Quest available only during event, otherwise cardboard -->
|
||
|
<a-entity id="leftHand" hand-tracking-controls="hand: left;"></a-entity>
|
||
|
<a-entity id="rightHand" hand-tracking-controls="hand: right;"></a-entity>
|
||
|
<a-entity id="camera-rig" set-initial-camera-from-data>
|
||
|
<a-sound autoplay="false" loop="true" src="src: url(Sounds/AMBIANCES/CEREMA_AMB_VILLE.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_CRACK_MAISON_01.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_CRACK_MAISON_02.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_CRACK_MAISON_03.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_CRACK_MAISON_04.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_SIRENNES_POMPIER.mp3)"></a-sound>
|
||
|
<a-sound class="randomfx" autoplay="false" loop="false" src="src: url(Sounds/FX/CEREMA_SIRENNE_COMMUNALE.mp3)"></a-sound>
|
||
|
<a-entity position="0 -1.2 0" sound="src: url(Sounds/FX/CEREMA_BOUCLE_EAU.mp3); loop:true; autoplay:false; volume:0;"
|
||
|
animation="property: sound.volume; from:0; to: 1; dur: 9000; easing: linear; loop: false;"
|
||
|
></a-entity>
|
||
|
<a-entity camera look-controls hud>
|
||
|
<!-- consider time as HUD text display-->
|
||
|
<a-entity cursor="fuse: true; fuseTimeout: 500"
|
||
|
position="0 0 -3"
|
||
|
geometry="primitive: ring; radiusInner: 0.04; radiusOuter: 0.05"
|
||
|
material="color: orange; shader: flat">
|
||
|
</a-entity>
|
||
|
</a-entity>
|
||
|
<a-gltf-model resetanimations="" position="2 2 4" scale="50 50 50" rotation="29 -160 -10" gltf-model="Visuals/ASSETS_3D/AA_Reset.glb" opacity="1"></a-gltf-model>
|
||
|
</a-entity>
|
||
|
<a-entity id="meshlamp" gltf-model="Visuals/EXPORTS/EXPORTS_gltf/ModelsSolo/AA_Ruelle_Lampadaire.glb" instanced-mesh ></a-entity>
|
||
|
<a-entity id="meshtree" position="-25 0 9" gltf-model="Visuals/EXPORTS/EXPORTS_gltf/AA_Ruelle_Arbre.glb" instanced-mesh ></a-entity>
|
||
|
|
||
|
<a-plane id="ground" visible=false position="0 -.5 0" rotation="-90 0 0" width="1000" height="1000" color="lightgrey"></a-plane>
|
||
|
<a-entity light="type: ambient; color: #BBB"></a-entity>
|
||
|
<a-entity id="sun" light="type: directional; color: #FFF; castShadow:true;" position="-5 5 0"
|
||
|
animation="property: light.intensity; from:0.1; to: 0.6; dur: 20000; easing: linear; loop: true; dir:alternate;"
|
||
|
animation__pos="property: position; from:-50 50 100; to: 50 50 100; dur: 20000; easing: linear; loop: true">
|
||
|
</a-entity>
|
||
|
|
||
|
<a-entity position="0 -0.10 0" scale="1.3 1 1.3" osm id="osm"></a-entity>
|
||
|
|
||
|
<!-- offset on test data for easier vr testing -->
|
||
|
<a-gltf-model class="models_ruelle" visible="" position="-139.95628 0.73 -34.3154" scale="1.2 1.2 1.2" rotation="0 -111.8545396388247 0" gltf-model="Visuals/EXPORTS/EXPORTS_gltf/AA_Quaidelaruelle.glb" listosmways=""></a-gltf-model>
|
||
|
<a-gltf-model class="models_baquets" visible="" position="-28.39341 0 10.05424" scale="" rotation="0 66.68655777527651 0" gltf-model="Visuals/EXPORTS/EXPORTS_gltf/AA_Citedesbaquets.glb" listosmways=""></a-gltf-model>
|
||
|
<a-plane id="landscape" position="1135.38323 -36.66906 -1784.89792" scale="50 50 50" rotation="-90.00021045914971 -96.34743691360897 0" width="100" height="100" geometry="segmentsHeight: 128; segmentsWidth: 128" material="src: ign_terrain_cached.jpg; displacementScale: 5; displacementMap: ign_elevation_cached.jpg" visible=""></a-plane>
|
||
|
|
||
|
<a-plane id="watersim" cerema="" position="0 -51.55 0" rotation="-90 90 0" src="Visuals/EAU/Eau_01.jpg"
|
||
|
material="displacementScale: 0.04; wireframe: false; displacementMap: "
|
||
|
geometry="segmentsHeight: 128; segmentsWidth: 128" scale="1000 1000 100"></a-plane>
|
||
|
<!-- animation might fail due texture.neddUpdate unset -->
|
||
|
|
||
|
<a-image id="debug_baquet_osm" src="baquet_osm.png" visible=false position="-235 1.1 145" rotation="-90 90 0" scale="200 200 200" material="opacity: 0.5" class=debug></a-image>
|
||
|
<a-image id="debug_ruelle_osm" src="ruelle_osm.png" visible=false position="-40 1 -220" rotation="-90 90 0" material="opacity: 0.5" scale="200 200 200" class="debug"></a-image>
|
||
|
<a-image id="debug_sim_cerema" src="" visible=false position="0 1.2 0" rotation="-90 90 0" material="opacity: 0.5" scale="1000 1000 1000" class="debug"></a-image>
|
||
|
<a-sky src="cloudsky.jpg"
|
||
|
animation="from: #000; to: #aaf; loop: true; dur: 20000; dir: alternate; loop:true; property: components.material.material.color; type: color;"
|
||
|
animation_texture="property: material.offset; to: 1 1; dur: 200; easing: linear; loop: true; dir: alternate;"
|
||
|
></a-sky>
|
||
|
</a-scene>
|
||
|
<!--
|
||
|
live layers (deactivated by default)
|
||
|
|
||
|
material="src: https://wxs.ign.fr/ortho/geoportail/r/wms?LAYERS=ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO&EXCEPTIONS=text/xml&FORMAT=image/jpeg&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=CRS:84&BBOX=0.470286,49.338484,0.556117,49.381417&WIDTH=4096&HEIGHT=4096; displacementScale: 20; displacementMap: https://wxs.ign.fr/altimetrie/geoportail/r/wms?LAYERS=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES&EXCEPTIONS=text/xml&FORMAT=image/jpeg&SERVICE=WMS&VERSION=1.3.0&REQUEST=GETMAP&STYLES=&CRS=CRS:84&BBOX=0.470286,49.338484,0.556117,49.381417&WIDTH=1024&HEIGHT=1024;" ></a-plane>
|
||
|
-->
|
||
|
</body>
|
||
|
</html>
|