Gerstner water
This commit is contained in:
		
							
								
								
									
										123
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								index.html
									
									
									
									
									
								
							| @ -17,6 +17,129 @@ | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		</script> | 		</script> | ||||||
|  | 		<script id="vertexShader" type="x-shader/x-vertex"> | ||||||
|  | 			uniform mat4 textureMatrix; | ||||||
|  | 			uniform float time; | ||||||
|  |  | ||||||
|  | 			varying vec4 mirrorCoord; | ||||||
|  | 			varying vec4 worldPosition; | ||||||
|  |  | ||||||
|  | 			#include <common> | ||||||
|  | 			#include <fog_pars_vertex> | ||||||
|  | 			#include <shadowmap_pars_vertex> | ||||||
|  | 			#include <logdepthbuf_pars_vertex> | ||||||
|  |  | ||||||
|  | 			uniform vec4 waveA; | ||||||
|  | 			uniform vec4 waveB; | ||||||
|  | 			uniform vec4 waveC; | ||||||
|  |  | ||||||
|  | 			vec3 GerstnerWave (vec4 wave, vec3 p) { | ||||||
|  | 				float steepness = wave.z; | ||||||
|  | 				float wavelength = wave.w; | ||||||
|  | 				float k = 2.0 * PI / wavelength; | ||||||
|  | 				float c = sqrt(9.8 / k); | ||||||
|  | 				vec2 d = normalize(wave.xy); | ||||||
|  | 				float f = k * (dot(d, p.xy) - c * time); | ||||||
|  | 				float a = steepness / k; | ||||||
|  |  | ||||||
|  | 				return vec3( | ||||||
|  | 					d.x * (a * cos(f)), | ||||||
|  | 					d.y * (a * cos(f)), | ||||||
|  | 					a * sin(f) | ||||||
|  | 				); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			void main() { | ||||||
|  | 				mirrorCoord = modelMatrix * vec4( position, 1.0 ); | ||||||
|  | 				worldPosition = mirrorCoord.xyzw; | ||||||
|  | 				mirrorCoord = textureMatrix * mirrorCoord; | ||||||
|  |  | ||||||
|  | 				vec3 p = position.xyz; | ||||||
|  | 				p += GerstnerWave(waveA, position.xyz); | ||||||
|  | 				p += GerstnerWave(waveB, position.xyz); | ||||||
|  | 				p += GerstnerWave(waveC, position.xyz); | ||||||
|  | 				gl_Position = projectionMatrix * modelViewMatrix * vec4( p.x, p.y, p.z, 1.0); | ||||||
|  |  | ||||||
|  | 				#include <beginnormal_vertex> | ||||||
|  | 				#include <defaultnormal_vertex> | ||||||
|  | 				#include <logdepthbuf_vertex> | ||||||
|  | 				#include <fog_vertex> | ||||||
|  | 				#include <shadowmap_vertex> | ||||||
|  | 			} | ||||||
|  | 		</script> | ||||||
|  | 		<script id="fragmentShader" type="x-shader/x-fragment"> | ||||||
|  | 			uniform sampler2D mirrorSampler; | ||||||
|  | 			uniform float alpha; | ||||||
|  | 			uniform float time; | ||||||
|  | 			uniform float size; | ||||||
|  | 			uniform float distortionScale; | ||||||
|  | 			uniform sampler2D normalSampler; | ||||||
|  | 			uniform vec3 sunColor; | ||||||
|  | 			uniform vec3 sunDirection; | ||||||
|  | 			uniform vec3 eye; | ||||||
|  | 			uniform vec3 waterColor; | ||||||
|  |  | ||||||
|  | 			varying vec4 mirrorCoord; | ||||||
|  | 			varying vec4 worldPosition; | ||||||
|  |  | ||||||
|  | 			vec4 getNoise( vec2 uv ) { | ||||||
|  | 				vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0); | ||||||
|  | 				vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 ); | ||||||
|  | 				vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 ); | ||||||
|  | 				vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 ); | ||||||
|  | 				vec4 noise = texture2D( normalSampler, uv0 ) + | ||||||
|  | 					texture2D( normalSampler, uv1 ) + | ||||||
|  | 					texture2D( normalSampler, uv2 ) + | ||||||
|  | 					texture2D( normalSampler, uv3 ); | ||||||
|  | 				return noise * 0.5 - 1.0; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) { | ||||||
|  | 				vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) ); | ||||||
|  | 				float direction = max( 0.0, dot( eyeDirection, reflection ) ); | ||||||
|  | 				specularColor += pow( direction, shiny ) * sunColor * spec; | ||||||
|  | 				diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			#include <common> | ||||||
|  | 			#include <packing> | ||||||
|  | 			#include <bsdfs> | ||||||
|  | 			#include <fog_pars_fragment> | ||||||
|  | 			#include <logdepthbuf_pars_fragment> | ||||||
|  | 			#include <lights_pars_begin> | ||||||
|  | 			#include <shadowmap_pars_fragment> | ||||||
|  | 			#include <shadowmask_pars_fragment> | ||||||
|  |  | ||||||
|  | 			void main() { | ||||||
|  |  | ||||||
|  | 				#include <logdepthbuf_fragment> | ||||||
|  | 				vec4 noise = getNoise( worldPosition.xz * size ); | ||||||
|  | 				vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) ); | ||||||
|  |  | ||||||
|  | 				vec3 diffuseLight = vec3(0.0); | ||||||
|  | 				vec3 specularLight = vec3(0.0); | ||||||
|  |  | ||||||
|  | 				vec3 worldToEye = eye-worldPosition.xyz; | ||||||
|  | 				vec3 eyeDirection = normalize( worldToEye ); | ||||||
|  | 				sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight ); | ||||||
|  |  | ||||||
|  | 				float distance = length(worldToEye); | ||||||
|  |  | ||||||
|  | 				vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale; | ||||||
|  | 				vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.w + distortion ) ); | ||||||
|  |  | ||||||
|  | 				float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 ); | ||||||
|  | 				float rf0 = 0.3; | ||||||
|  | 				float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 ); | ||||||
|  | 				vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor; | ||||||
|  | 				vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance); | ||||||
|  | 				vec3 outgoingLight = albedo; | ||||||
|  | 				gl_FragColor = vec4( outgoingLight, alpha ); | ||||||
|  |  | ||||||
|  | 				#include <tonemapping_fragment> | ||||||
|  | 				#include <fog_fragment> | ||||||
|  | 			} | ||||||
|  | 		</script> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
| 		<div id="container"></div> | 		<div id="container"></div> | ||||||
|  | |||||||
							
								
								
									
										588
									
								
								main.js
									
									
									
									
									
								
							
							
						
						
									
										588
									
								
								main.js
									
									
									
									
									
								
							| @ -1,14 +1,12 @@ | |||||||
|  |  | ||||||
| import * as THREE from 'three'; | import * as THREE from 'three'; | ||||||
|  |  | ||||||
| import Stats from 'three/addons/libs/stats.module.js'; | import Stats from 'three/addons/libs/stats.module.js'; | ||||||
|  |  | ||||||
| import { Octree } from 'three/addons/math/Octree.js'; | import { Octree } from 'three/addons/math/Octree.js'; | ||||||
| import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js'; | import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js'; | ||||||
|  |  | ||||||
| import { Capsule } from 'three/addons/math/Capsule.js'; | import { Capsule } from 'three/addons/math/Capsule.js'; | ||||||
|  |  | ||||||
| import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; | import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; | ||||||
|  |  | ||||||
| import { Water } from 'three/addons/objects/Water.js'; | import { Water } from 'three/addons/objects/Water.js'; | ||||||
| import { Sky } from 'three/addons/objects/Sky.js'; | import { Sky } from 'three/addons/objects/Sky.js'; | ||||||
|  |  | ||||||
| @ -16,11 +14,20 @@ import MazeMesh from './MazeMesh.js'; | |||||||
|  |  | ||||||
| const mazeLength = 23 | const mazeLength = 23 | ||||||
| const mazeWidth = 23 | const mazeWidth = 23 | ||||||
| const latitude   = THREE.MathUtils.degToRad(35) |  | ||||||
| const longitude  = THREE.MathUtils.degToRad(25) |  | ||||||
|  |  | ||||||
| let showGUI = window.location.search.includes("debug") | const parameters = { | ||||||
| let showStats = window.location.search.includes("stats") |     elevation: 90 * Math.random(), | ||||||
|  |     azimuth: 180 * Math.random(), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const waves = { | ||||||
|  |     A: { direction: 0, steepness: 0.05, wavelength: 3 }, | ||||||
|  |     B: { direction: 30, steepness: 0.10, wavelength: 6 }, | ||||||
|  |     C: { direction: 60, steepness: 0.05, wavelength: 1.5 }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const showParam = window.location.search.includes("param") | ||||||
|  | const showStats = window.location.search.includes("stats") | ||||||
|  |  | ||||||
| const ambiance = new Audio("snd/ambiance.mp3") | const ambiance = new Audio("snd/ambiance.mp3") | ||||||
| ambiance.loop = true | ambiance.loop = true | ||||||
| @ -29,15 +36,24 @@ piano.loop          = false | |||||||
|  |  | ||||||
| const loadMngr = new THREE.LoadingManager(); | const loadMngr = new THREE.LoadingManager(); | ||||||
| const loader = new THREE.TextureLoader(loadMngr); | const loader = new THREE.TextureLoader(loadMngr); | ||||||
| const waterTexture  = loader.load('img/waternormals.jpg'); |  | ||||||
| const groundTexture = loader.load('img/pavement.jpg'); |  | ||||||
| const wallTexture   = loader.load('img/wall.jpg'); |  | ||||||
| const woodTexture   = loader.load('img/wood.jpg'); |  | ||||||
| loadMngr.onLoad = () => { | loadMngr.onLoad = () => { | ||||||
|     animate(); |     animate(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const clock = new THREE.Clock(); | // | ||||||
|  |  | ||||||
|  | const container = document.getElementById('container'); | ||||||
|  |  | ||||||
|  | const renderer = new THREE.WebGLRenderer({ | ||||||
|  |     powerPreference: "high-performance", | ||||||
|  |     antialias: true, | ||||||
|  | }); | ||||||
|  | renderer.setPixelRatio(window.devicePixelRatio); | ||||||
|  | renderer.setSize(window.innerWidth, window.innerHeight); | ||||||
|  | renderer.toneMapping = THREE.ACESFilmicToneMapping; | ||||||
|  | renderer.shadowMap.enabled = true; | ||||||
|  | renderer.shadowMap.type = THREE.PCFShadowMap; | ||||||
|  | container.appendChild(renderer.domElement); | ||||||
|  |  | ||||||
| const scene = new THREE.Scene(); | const scene = new THREE.Scene(); | ||||||
|  |  | ||||||
| @ -48,38 +64,82 @@ camera.position.set( 0, 25, 0 ); | |||||||
| const worldOctree = new Octree(); | const worldOctree = new Octree(); | ||||||
| const raftOctree = new Octree(); | const raftOctree = new Octree(); | ||||||
|  |  | ||||||
| const container = document.getElementById( 'container' ); |  | ||||||
|  |  | ||||||
| const renderer = new THREE.WebGLRenderer( { antialias: true } ); |  | ||||||
| renderer.setPixelRatio( window.devicePixelRatio ); |  | ||||||
| renderer.setSize( window.innerWidth, window.innerHeight ); |  | ||||||
| renderer.shadowMap.enabled = true; |  | ||||||
| renderer.shadowMap.type    = THREE.PCFShadowMap   ; |  | ||||||
| renderer.toneMapping       = THREE.ACESFilmicToneMapping; |  | ||||||
| container.appendChild( renderer.domElement ); |  | ||||||
|  |  | ||||||
| // Water | // Water | ||||||
| waterTexture.wrapS = waterTexture.wrapT = THREE.RepeatWrapping; |  | ||||||
| const ocean = new Water( | const waterGeometry = new THREE.PlaneGeometry(2048, 2048, 512, 512); | ||||||
|     new THREE.PlaneGeometry( 1000, 1000 ), |  | ||||||
|     { | const ocean = new Water(waterGeometry, { | ||||||
|     textureWidth: 512, |     textureWidth: 512, | ||||||
|     textureHeight: 512, |     textureHeight: 512, | ||||||
|         waterNormals   : waterTexture, |     waterNormals: loader.load( | ||||||
|  |         'img/waternormals.jpg', | ||||||
|  |         function (texture) { | ||||||
|  |             texture.wrapS = texture.wrapT = THREE.RepeatWrapping; | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|     sunDirection: new THREE.Vector3(), |     sunDirection: new THREE.Vector3(), | ||||||
|     sunColor: 0xffffff, |     sunColor: 0xffffff, | ||||||
|     waterColor: 0x001e0f, |     waterColor: 0x001e0f, | ||||||
|     distortionScale: 3.7, |     distortionScale: 3.7, | ||||||
|     fog: scene.fog !== undefined, |     fog: scene.fog !== undefined, | ||||||
|         alpha          : 0.7 |     alpha: 0.9 | ||||||
|     } | }); | ||||||
| ); | ocean.rotation.x = - Math.PI / 2; | ||||||
| ocean.rotation.x           = - Math.PI * 0.5; | ocean.position.y = -0.2; | ||||||
| ocean.position.y           = -.01 |  | ||||||
| ocean.receiveShadow        = true; |  | ||||||
| ocean.material.transparent = true; | ocean.material.transparent = true; | ||||||
|  | ocean.material.onBeforeCompile = function (shader) { | ||||||
|  |  | ||||||
|  |     shader.uniforms.size = { value: 6 } | ||||||
|  |  | ||||||
|  |     shader.uniforms.waveA = { | ||||||
|  |         value: [ | ||||||
|  |             Math.sin((waves.A.direction * Math.PI) / 180), | ||||||
|  |             Math.cos((waves.A.direction * Math.PI) / 180), | ||||||
|  |             waves.A.steepness, | ||||||
|  |             waves.A.wavelength, | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  |     shader.uniforms.waveB = { | ||||||
|  |         value: [ | ||||||
|  |             Math.sin((waves.B.direction * Math.PI) / 180), | ||||||
|  |             Math.cos((waves.B.direction * Math.PI) / 180), | ||||||
|  |             waves.B.steepness, | ||||||
|  |             waves.B.wavelength, | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  |     shader.uniforms.waveC = { | ||||||
|  |         value: [ | ||||||
|  |             Math.sin((waves.C.direction * Math.PI) / 180), | ||||||
|  |             Math.cos((waves.C.direction * Math.PI) / 180), | ||||||
|  |             waves.C.steepness, | ||||||
|  |             waves.C.wavelength, | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  |     shader.vertexShader = document.getElementById('vertexShader').textContent; | ||||||
|  |     shader.fragmentShader = document.getElementById('fragmentShader').textContent; | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
| scene.add(ocean); | scene.add(ocean); | ||||||
|  |  | ||||||
|  | // Skybox | ||||||
|  |  | ||||||
|  | const sun = new THREE.Vector3(); | ||||||
|  |  | ||||||
|  | const sky = new Sky(); | ||||||
|  | sky.scale.setScalar(10000); | ||||||
|  | scene.add(sky); | ||||||
|  |  | ||||||
|  | const skyUniforms = sky.material.uniforms; | ||||||
|  |  | ||||||
|  | skyUniforms['turbidity'].value = 10; | ||||||
|  | skyUniforms['rayleigh'].value = 2; | ||||||
|  | skyUniforms['mieCoefficient'].value = 0.005; | ||||||
|  | skyUniforms['mieDirectionalG'].value = 0.8; | ||||||
|  |  | ||||||
|  | const pmremGenerator = new THREE.PMREMGenerator(renderer); | ||||||
|  |  | ||||||
| // Lights | // Lights | ||||||
|  |  | ||||||
| const ambientLight = new THREE.AmbientLight(0x404040, 1); // soft white light | const ambientLight = new THREE.AmbientLight(0x404040, 1); // soft white light | ||||||
| @ -99,119 +159,12 @@ sunLight.shadow.radius         = 4; | |||||||
| sunLight.target = camera | sunLight.target = camera | ||||||
| scene.add(sunLight); | scene.add(sunLight); | ||||||
|  |  | ||||||
| const torchLight = new THREE.SpotLight(0xffffe8, 1, mazeLength/2, .45, 1) |  | ||||||
| scene.add( torchLight ); |  | ||||||
| scene.add( torchLight.target ); |  | ||||||
|  |  | ||||||
| // Skybox |  | ||||||
|  |  | ||||||
| const sun = new THREE.Vector3(); |  | ||||||
|  |  | ||||||
| const sky = new Sky(); |  | ||||||
| sky.scale.setScalar( 10000 ); |  | ||||||
| scene.add( sky ); |  | ||||||
|  |  | ||||||
| const skyUniforms = sky.material.uniforms; |  | ||||||
|  |  | ||||||
| skyUniforms[ 'turbidity' ].value = 10; |  | ||||||
| skyUniforms[ 'rayleigh' ].value = 2; |  | ||||||
| skyUniforms[ 'mieCoefficient' ].value = 0.005; |  | ||||||
| skyUniforms[ 'mieDirectionalG' ].value = 0.8; |  | ||||||
|  |  | ||||||
| const parameters = { |  | ||||||
|     elevation: 70, |  | ||||||
|     azimuth: 160 |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const pmremGenerator = new THREE.PMREMGenerator( renderer ); |  | ||||||
| let renderTarget; |  | ||||||
|  |  | ||||||
| const today       = new Date() |  | ||||||
| const startOfYear = new Date(today.getFullYear(), 0, 0); |  | ||||||
| const diff        = today - startOfYear; |  | ||||||
| const oneDay      = 1000 * 60 * 60 * 24; |  | ||||||
| const dayOfYear   = Math.floor(diff / oneDay); |  | ||||||
| const declination = 0.40928 * Math.sin(2*Math.PI*(dayOfYear+284)/365) |  | ||||||
| const startHour   = 24 * Math.random() |  | ||||||
|  |  | ||||||
| function updateSun() { |  | ||||||
|      |  | ||||||
|     let elevation, azimuth |  | ||||||
|     if ( showGUI ) { |  | ||||||
|      |  | ||||||
|         elevation = THREE.MathUtils.degToRad( parameters.elevation ); |  | ||||||
|         azimuth   = THREE.MathUtils.degToRad( parameters.azimuth ); |  | ||||||
|  |  | ||||||
|     } else { |  | ||||||
|  |  | ||||||
|         const time      = clock.elapsedTime ; |  | ||||||
|         const hour      = ( startHour + time / 1440 ) % 24 |  | ||||||
|         const hourAngle = Math.PI * (1-hour/12) |  | ||||||
|               elevation = Math.asin( Math.sin(declination)*Math.sin(latitude) + Math.cos(declination)*Math.cos(latitude)*Math.cos(hourAngle) ) |  | ||||||
|               azimuth   = -Math.PI/2 + Math.asin( Math.cos(declination)*Math.sin(hourAngle)/Math.cos(elevation) ) |  | ||||||
|      |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const phi   = Math.PI/2 - elevation |  | ||||||
|     const theta = azimuth |  | ||||||
|  |  | ||||||
|     sun.setFromSphericalCoords( 100, phi, theta ); |  | ||||||
|  |  | ||||||
|     sky.material.uniforms[ 'sunPosition' ].value.copy( sun ); |  | ||||||
|     ocean.material.uniforms[ 'sunDirection' ].value.copy( sun ).normalize(); |  | ||||||
|     ambientLight.intensity = 0.5 + Math.max( elevation, 0 )/Math.PI; |  | ||||||
|  |  | ||||||
|     if ( elevation >= 0 ) { |  | ||||||
|  |  | ||||||
|         sunLight.visible   = true |  | ||||||
|         torchLight.visible = false |  | ||||||
|  |  | ||||||
|     }  else { |  | ||||||
|  |  | ||||||
|         sunLight.visible   = false |  | ||||||
|         torchLight.visible = true |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ( renderTarget !== undefined ) renderTarget.dispose(); |  | ||||||
|  |  | ||||||
|     renderTarget = pmremGenerator.fromScene( sky ); |  | ||||||
|  |  | ||||||
|     scene.environment = renderTarget.texture; |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| updateSun(); | updateSun(); | ||||||
| const updateSunIntervalId = setInterval( updateSun, 100 ); |  | ||||||
|  |  | ||||||
| // Ground |  | ||||||
|  |  | ||||||
| const groundGeometry = new THREE.PlaneGeometry(mazeLength, mazeWidth) |  | ||||||
| groundTexture.wrapS  = groundTexture.wrapT = THREE.RepeatWrapping |  | ||||||
| groundTexture.repeat.set(mazeLength/2, mazeWidth/2) |  | ||||||
| const groundMaterial = new THREE.MeshPhongMaterial( { |  | ||||||
|     map       : groundTexture, |  | ||||||
|     color     : 0xFFFFFF, |  | ||||||
|     emissive  : 0, |  | ||||||
|     specular  : 0x000000, |  | ||||||
|     shininess : 5, |  | ||||||
|     bumpMap   : groundTexture, |  | ||||||
|     bumpScale : .02, |  | ||||||
|     depthFunc : 3, |  | ||||||
|     depthTest : true, |  | ||||||
|     depthWrite: true |  | ||||||
| } ) |  | ||||||
| const ground = new THREE.Mesh( groundGeometry, groundMaterial ) |  | ||||||
| ground.rotation.x = - Math.PI / 2; |  | ||||||
| ground.receiveShadow = true; |  | ||||||
| ground.matrixAutoUpdate = false |  | ||||||
| ground.updateMatrix(); |  | ||||||
| scene.add(ground) |  | ||||||
| worldOctree.fromGraphNode( ground ) |  | ||||||
|  |  | ||||||
| // Raft | // Raft | ||||||
|  |  | ||||||
| const raftGeometry = new THREE.BoxGeometry(1.8, .1, .9, 1, 1, 8) | const raftGeometry = new THREE.BoxGeometry(1.8, .1, .9, 1, 1, 8) | ||||||
|  | const woodTexture = loader.load('img/wood.jpg'); | ||||||
| const raftMaterial = new THREE.MeshPhongMaterial({ | const raftMaterial = new THREE.MeshPhongMaterial({ | ||||||
|     map: woodTexture, |     map: woodTexture, | ||||||
|     color: 0xFFFFFF, |     color: 0xFFFFFF, | ||||||
| @ -227,18 +180,16 @@ const raftMaterial = new THREE.MeshPhongMaterial( { | |||||||
|     displacementScale: -0.08 |     displacementScale: -0.08 | ||||||
| }) | }) | ||||||
| const raft = new THREE.Mesh(raftGeometry, raftMaterial) | const raft = new THREE.Mesh(raftGeometry, raftMaterial) | ||||||
| raft.position.set( .2, 0, -1 - mazeWidth/2 ) | raft.position.set(.2, ocean.position.y, -1 - mazeWidth / 2) | ||||||
| raft.rotation.y = 1.4 | raft.rotation.y = 1.4 | ||||||
| raft.rotation.order = 'ZXY'; |  | ||||||
| raft.castShadow = true; | raft.castShadow = true; | ||||||
| scene.add(raft) |  | ||||||
| worldOctree.fromGraphNode(raft) | worldOctree.fromGraphNode(raft) | ||||||
| raftOctree.fromGraphNode(raft) | raftOctree.fromGraphNode(raft) | ||||||
|  | scene.add(raft) | ||||||
| camera.lookAt( raft.position.x, raft.position.y, raft.position.z ); |  | ||||||
|  |  | ||||||
| // Maze | // Maze | ||||||
|  |  | ||||||
|  | const wallTexture = loader.load('img/wall.jpg'); | ||||||
| const wallMaterial = new THREE.MeshPhongMaterial({ | const wallMaterial = new THREE.MeshPhongMaterial({ | ||||||
|     map: wallTexture, |     map: wallTexture, | ||||||
|     color: 0xFCF8E5, |     color: 0xFCF8E5, | ||||||
| @ -252,6 +203,8 @@ const wallMaterial = new THREE.MeshPhongMaterial( { | |||||||
|     depthWrite: true |     depthWrite: true | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | // Maze | ||||||
|  |  | ||||||
| const maze = new MazeMesh(mazeLength, mazeWidth, wallMaterial); | const maze = new MazeMesh(mazeLength, mazeWidth, wallMaterial); | ||||||
| maze.castShadow = true; | maze.castShadow = true; | ||||||
| maze.receiveShadow = true; | maze.receiveShadow = true; | ||||||
| @ -269,55 +222,201 @@ for ( let i=0; i<maze.count; i++ ) { | |||||||
|     worldOctree.fromGraphNode(clone) |     worldOctree.fromGraphNode(clone) | ||||||
| } | } | ||||||
|  |  | ||||||
| // debug | // Ground | ||||||
|  |  | ||||||
| let stats, octreeHelper, gui | const pavementTexture = loader.load( | ||||||
| if ( showGUI ) { |     'img/pavement.jpg', | ||||||
|  |     texture => { | ||||||
|  |         texture.wrapS = texture.wrapT = THREE.RepeatWrapping | ||||||
|  |         texture.repeat.set(mazeLength / 2, mazeWidth / 2) | ||||||
|  |     } | ||||||
|  | ); | ||||||
|  | const groundGeometry = new THREE.BoxGeometry(mazeLength, mazeWidth, 1) | ||||||
|  | const groundMaterial = new THREE.MeshPhongMaterial({ | ||||||
|  |     map: pavementTexture, | ||||||
|  |     color: 0xFFFFFF, | ||||||
|  |     emissive: 0, | ||||||
|  |     specular: 0x000000, | ||||||
|  |     shininess: 5, | ||||||
|  |     bumpMap: pavementTexture, | ||||||
|  |     bumpScale: .02, | ||||||
|  |     depthFunc: 3, | ||||||
|  |     depthTest: true, | ||||||
|  |     depthWrite: true | ||||||
|  | }) | ||||||
|  | const sideGroundTexture = wallTexture.clone() | ||||||
|  | sideGroundTexture.wrapS = sideGroundTexture.wrapT = THREE.RepeatWrapping | ||||||
|  | sideGroundTexture.repeat.set(mazeLength, 1) | ||||||
|  | const sideGroundMaterial = new THREE.MeshPhongMaterial({ | ||||||
|  |     map: sideGroundTexture, | ||||||
|  |     color: 0xFCF8E5, | ||||||
|  |     emissive: 0, | ||||||
|  |     specular: 0x505050, | ||||||
|  |     shininess: 4, | ||||||
|  |     bumpMap: sideGroundTexture, | ||||||
|  |     bumpScale: .01, | ||||||
|  |     depthFunc: 3, | ||||||
|  |     depthTest: true, | ||||||
|  |     depthWrite: true | ||||||
|  | }) | ||||||
|  | const ground = new THREE.Mesh( | ||||||
|  |     groundGeometry, | ||||||
|  |     [ | ||||||
|  |         sideGroundMaterial, | ||||||
|  |         sideGroundMaterial, | ||||||
|  |         sideGroundMaterial, | ||||||
|  |         sideGroundMaterial, | ||||||
|  |         groundMaterial, | ||||||
|  |         groundMaterial, | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | ground.rotation.x = - Math.PI / 2; | ||||||
|  | ground.position.y = -0.5 | ||||||
|  | ground.receiveShadow = true; | ||||||
|  | ground.matrixAutoUpdate = false | ||||||
|  | ground.updateMatrix(); | ||||||
|  | scene.add(ground) | ||||||
|  |  | ||||||
|     gui = new GUI( { width: 200 } ); | const groundCollisioner = new THREE.Mesh( | ||||||
|  |     new THREE.PlaneGeometry(mazeLength, mazeWidth) | ||||||
|  | ) | ||||||
|  | groundCollisioner.rotation.x = - Math.PI / 2; | ||||||
|  | worldOctree.fromGraphNode(groundCollisioner) | ||||||
|  |  | ||||||
|  | // | ||||||
|  |  | ||||||
|  | const stats = new Stats(); | ||||||
|  | if (showStats) container.appendChild(stats.dom); | ||||||
|  |  | ||||||
|  | // GUI | ||||||
|  |  | ||||||
|  | if (showParam) { | ||||||
|  |  | ||||||
|  |     const gui = new GUI(); | ||||||
|  |  | ||||||
|     octreeHelper = new OctreeHelper( worldOctree ); |  | ||||||
|     octreeHelper.visible = false; |  | ||||||
|     scene.add( octreeHelper ); |  | ||||||
|     const lightHelper = new THREE.DirectionalLightHelper(sunLight, .5) |     const lightHelper = new THREE.DirectionalLightHelper(sunLight, .5) | ||||||
|     lightHelper.position.copy(maze.start) |     lightHelper.position.copy(maze.start) | ||||||
|     lightHelper.visible = false; |     lightHelper.visible = false; | ||||||
|     scene.add( lightHelper ); |  | ||||||
|     var cameraHelper = new THREE.CameraHelper(sunLight.shadow.camera); |     const octreeHelper = new OctreeHelper(worldOctree); | ||||||
|     cameraHelper.visible = false; |     octreeHelper.visible = false; | ||||||
|     scene.add(cameraHelper) |     scene.add(octreeHelper); | ||||||
|     const showHelper = gui.add({ helpers: false }, "helpers") |     const showHelper = gui.add({ helpers: false }, "helpers") | ||||||
|     showHelper.onChange(function (value) { |     showHelper.onChange(function (value) { | ||||||
|  |  | ||||||
|         octreeHelper.visible = value; |  | ||||||
|         lightHelper.visible = value; |         lightHelper.visible = value; | ||||||
|         cameraHelper.visible = value; |         octreeHelper.visible = value; | ||||||
|  |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const folderSky = gui.addFolder('Sky'); |     const folderSky = gui.addFolder('Sky'); | ||||||
|     folderSky.add( parameters, 'elevation', -90, 90, 0.1 ).onChange( updateSun ); |     folderSky.add(parameters, 'elevation', 0, 90, 0.1).onChange(updateSun); | ||||||
|     folderSky.add(parameters, 'azimuth', - 180, 180, 0.1).onChange(updateSun); |     folderSky.add(parameters, 'azimuth', - 180, 180, 0.1).onChange(updateSun); | ||||||
|     folderSky.open(); |     folderSky.open(); | ||||||
|  |  | ||||||
|     const waterUniforms = ocean.material.uniforms; |     const waterUniforms = ocean.material.uniforms; | ||||||
|  |  | ||||||
|     const folderWater = gui.addFolder('Water'); |     const folderWater = gui.addFolder('Water'); | ||||||
|     folderWater.add( waterUniforms.distortionScale, 'value', 0, 8, 0.1 ).name( 'distortionScale' ); |     folderWater | ||||||
|  |         .add(waterUniforms.distortionScale, 'value', 0, 8, 0.1) | ||||||
|  |         .name('distortionScale'); | ||||||
|     folderWater.add(waterUniforms.size, 'value', 0.1, 10, 0.1).name('size'); |     folderWater.add(waterUniforms.size, 'value', 0.1, 10, 0.1).name('size'); | ||||||
|  |     folderWater.add(ocean.material, 'wireframe'); | ||||||
|     folderWater.open(); |     folderWater.open(); | ||||||
|  |  | ||||||
| } |     const waveAFolder = gui.addFolder('Wave A'); | ||||||
|  |     waveAFolder | ||||||
|  |         .add(waves.A, 'direction', 0, 359) | ||||||
|  |         .name('Direction') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
| if ( showStats ) { |             const x = (v * Math.PI) / 180; | ||||||
|  |             ocean.material.uniforms.waveA.value[0] = Math.sin(x); | ||||||
|  |             ocean.material.uniforms.waveA.value[1] = Math.cos(x); | ||||||
|  |  | ||||||
|     stats = new Stats(); |         }); | ||||||
|     stats.domElement.style.position = 'absolute'; |     waveAFolder | ||||||
|     stats.domElement.style.top = '0px'; |         .add(waves.A, 'steepness', 0, 1, 0.01) | ||||||
|     container.appendChild( stats.domElement ); |         .name('Steepness') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveA.value[2] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveAFolder | ||||||
|  |         .add(waves.A, 'wavelength', 1, 100) | ||||||
|  |         .name('Wavelength') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveA.value[3] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveAFolder.open(); | ||||||
|  |  | ||||||
|  |     const waveBFolder = gui.addFolder('Wave B'); | ||||||
|  |     waveBFolder | ||||||
|  |         .add(waves.B, 'direction', 0, 359) | ||||||
|  |         .name('Direction') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             const x = (v * Math.PI) / 180; | ||||||
|  |             ocean.material.uniforms.waveB.value[0] = Math.sin(x); | ||||||
|  |             ocean.material.uniforms.waveB.value[1] = Math.cos(x); | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveBFolder | ||||||
|  |         .add(waves.B, 'steepness', 0, 1, 0.01) | ||||||
|  |         .name('Steepness') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveB.value[2] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveBFolder | ||||||
|  |         .add(waves.B, 'wavelength', 1, 100) | ||||||
|  |         .name('Wavelength') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveB.value[3] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveBFolder.open(); | ||||||
|  |  | ||||||
|  |     const waveCFolder = gui.addFolder('Wave C'); | ||||||
|  |     waveCFolder | ||||||
|  |         .add(waves.C, 'direction', 0, 359) | ||||||
|  |         .name('Direction') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             const x = (v * Math.PI) / 180; | ||||||
|  |             ocean.material.uniforms.waveC.value[0] = Math.sin(x); | ||||||
|  |             ocean.material.uniforms.waveC.value[1] = Math.cos(x); | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveCFolder | ||||||
|  |         .add(waves.C, 'steepness', 0, 1, 0.01) | ||||||
|  |         .name('Steepness') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveC.value[2] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveCFolder | ||||||
|  |         .add(waves.C, 'wavelength', 1, 100) | ||||||
|  |         .name('Wavelength') | ||||||
|  |         .onChange((v) => { | ||||||
|  |  | ||||||
|  |             ocean.material.uniforms.waveC.value[3] = v; | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     waveCFolder.open(); | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // | ||||||
|  |  | ||||||
|  | const clock = new THREE.Clock(); | ||||||
|  |  | ||||||
| // Controls | // Controls | ||||||
|  |  | ||||||
| const GRAVITY = 30; | const GRAVITY = 30; | ||||||
| @ -379,17 +478,6 @@ document.body.addEventListener( 'mousemove', ( event ) => { | |||||||
|  |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| window.addEventListener( 'resize', onWindowResize ); |  | ||||||
|  |  | ||||||
| function onWindowResize() { |  | ||||||
|  |  | ||||||
|     camera.aspect = window.innerWidth / window.innerHeight; |  | ||||||
|     camera.updateProjectionMatrix(); |  | ||||||
|  |  | ||||||
|     renderer.setSize( window.innerWidth, window.innerHeight ); |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function playerCollisions() { | function playerCollisions() { | ||||||
|  |  | ||||||
|     if (!escaped && raftOctree.capsuleIntersect(playerCollider)) { |     if (!escaped && raftOctree.capsuleIntersect(playerCollider)) { | ||||||
| @ -428,13 +516,11 @@ function gameEnd() { | |||||||
|  |  | ||||||
| addEventListener("animationend", (event) => { | addEventListener("animationend", (event) => { | ||||||
|  |  | ||||||
|     clearInterval( updateSunIntervalId ); |  | ||||||
|     document.exitPointerLock(); |     document.exitPointerLock(); | ||||||
|     container.style.cursor = "default"; |     container.style.cursor = "default"; | ||||||
|  |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| function updatePlayer(deltaTime) { | function updatePlayer(deltaTime) { | ||||||
|  |  | ||||||
|     let damping = Math.exp(- 4 * deltaTime) - 1; |     let damping = Math.exp(- 4 * deltaTime) - 1; | ||||||
| @ -537,58 +623,87 @@ function teleportPlayerIfOob() { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const waves = { |  | ||||||
|     A: { |  | ||||||
|         direction: 0, |  | ||||||
|         steepness: 0.015, |  | ||||||
|         wavelength: 10, |  | ||||||
|     }, |  | ||||||
|     B: { |  | ||||||
|         direction: 30, |  | ||||||
|         steepness: 0.015, |  | ||||||
|         wavelength: 5, |  | ||||||
|     }, |  | ||||||
|     C: { |  | ||||||
|         direction: 60, |  | ||||||
|         steepness: 0.015, |  | ||||||
|         wavelength: 3, |  | ||||||
|     }, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function getWaveInfo(x, z, time) { | function getWaveInfo(x, z, time) { | ||||||
|     const pos      = new THREE.Vector3() |  | ||||||
|     const tangent  = new THREE.Vector3(1, 0, 0) |     const pos = new THREE.Vector3(); | ||||||
|     const binormal = new THREE.Vector3(0, 0, 1) |     const tangent = new THREE.Vector3(1, 0, 0); | ||||||
|     Object.keys(waves).forEach(function (wave) { |     const binormal = new THREE.Vector3(0, 0, 1); | ||||||
|         const w = waves[wave] |     Object.keys(waves).forEach((wave) => { | ||||||
|         const k = (Math.PI * 2) / w.wavelength |  | ||||||
|         const c = Math.sqrt(9.8 / k) |         const w = waves[wave]; | ||||||
|  |         const k = (Math.PI * 2) / w.wavelength; | ||||||
|  |         const c = Math.sqrt(9.8 / k); | ||||||
|         const d = new THREE.Vector2( |         const d = new THREE.Vector2( | ||||||
|             Math.sin((w.direction * Math.PI) / 180), |             Math.sin((w.direction * Math.PI) / 180), | ||||||
|             - Math.cos((w.direction * Math.PI) / 180) |             - Math.cos((w.direction * Math.PI) / 180) | ||||||
|         ) |         ); | ||||||
|         const f = k * (d.dot(new THREE.Vector2(x, z)) - c * time) |         const f = k * (d.dot(new THREE.Vector2(x, z)) - c * time); | ||||||
|         const a = w.steepness / k |         const a = w.steepness / k; | ||||||
|         pos.x += d.y * (a * Math.cos(f)) |  | ||||||
|         pos.y += a * Math.sin(f) |         pos.x += d.y * (a * Math.cos(f)); | ||||||
|         pos.z += d.x * (a * Math.cos(f)) |         pos.y += a * Math.sin(f); | ||||||
|         tangent.x += -d.x * d.x * (w.steepness * Math.sin(f)) |         pos.z += d.x * (a * Math.cos(f)); | ||||||
|         tangent.y += d.x * (w.steepness * Math.cos(f)) |  | ||||||
|         tangent.z += -d.x * d.y * (w.steepness * Math.sin(f)) |         tangent.x += - d.x * d.x * (w.steepness * Math.sin(f)); | ||||||
|         binormal.x += -d.x * d.y * (w.steepness * Math.sin(f)) |         tangent.y += d.x * (w.steepness * Math.cos(f)); | ||||||
|         binormal.y += d.y * (w.steepness * Math.cos(f)) |         tangent.z += - d.x * d.y * (w.steepness * Math.sin(f)); | ||||||
|         binormal.z += -d.y * d.y * (w.steepness * Math.sin(f)) |  | ||||||
|     }) |         binormal.x += - d.x * d.y * (w.steepness * Math.sin(f)); | ||||||
|     const normal = binormal.cross(tangent).normalize() |         binormal.y += d.y * (w.steepness * Math.cos(f)); | ||||||
|     return { |         binormal.z += - d.y * d.y * (w.steepness * Math.sin(f)); | ||||||
|         position: pos, |  | ||||||
|         normal: normal, |     }); | ||||||
|     } |  | ||||||
|  |     const normal = binormal.cross(tangent).normalize(); | ||||||
|  |  | ||||||
|  |     return { position: pos, normal: normal }; | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function updateRaft(delta) { | ||||||
|  |  | ||||||
|  |     const t = ocean.material.uniforms['time'].value; | ||||||
|  |  | ||||||
|  |     const waveInfo = getWaveInfo(raft.position.x, raft.position.z, t); | ||||||
|  |     raft.position.y = ocean.position.y + waveInfo.position.y; | ||||||
|  |     const quat = new THREE.Quaternion().setFromEuler( | ||||||
|  |         new THREE.Euler().setFromVector3(waveInfo.normal) | ||||||
|  |     ); | ||||||
|  |     raft.quaternion.rotateTowards(quat, delta * 0.5); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function updateSun() { | ||||||
|  |  | ||||||
|  |     const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); | ||||||
|  |     const theta = THREE.MathUtils.degToRad(parameters.azimuth); | ||||||
|  |  | ||||||
|  |     sun.setFromSphericalCoords(100, phi, theta); | ||||||
|  |  | ||||||
|  |     sky.material.uniforms['sunPosition'].value.copy(sun); | ||||||
|  |     ocean.material.uniforms['sunDirection'].value.copy(sun).normalize(); | ||||||
|  |  | ||||||
|  |     ambientLight.intensity = 0.5 + Math.sin(Math.max(THREE.MathUtils.degToRad(parameters.elevation), 0)); | ||||||
|  |  | ||||||
|  |     scene.environment = pmremGenerator.fromScene(sky).texture; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | window.addEventListener('resize', onWindowResize); | ||||||
|  |  | ||||||
|  | function onWindowResize() { | ||||||
|  |  | ||||||
|  |     camera.aspect = window.innerWidth / window.innerHeight; | ||||||
|  |     camera.updateProjectionMatrix(); | ||||||
|  |  | ||||||
|  |     renderer.setSize(window.innerWidth, window.innerHeight); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| function animate() { | function animate() { | ||||||
|  |  | ||||||
|  |     requestAnimationFrame(animate); | ||||||
|  |  | ||||||
|     const delta = Math.min(0.05, clock.getDelta()) |     const delta = Math.min(0.05, clock.getDelta()) | ||||||
|     const deltaTime = delta / STEPS_PER_FRAME; |     const deltaTime = delta / STEPS_PER_FRAME; | ||||||
|  |  | ||||||
| @ -605,35 +720,16 @@ function animate() { | |||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const time = clock.elapsedTime; |     if (camera.position.y > 3.5) | ||||||
|  |         camera.lookAt(raft.position.x, raft.position.y, raft.position.z); | ||||||
|     ocean.material.uniforms[ 'time' ].value += 1.0 / 100.0; |  | ||||||
|     const waveInfo = getWaveInfo(raft.position.x, raft.position.z, time) |  | ||||||
|     raft.position.y = waveInfo.position.y |  | ||||||
|     const quat = new THREE.Quaternion().setFromEuler( |  | ||||||
|         new THREE.Euler(waveInfo.normal.x, waveInfo.normal.y, waveInfo.normal.z) |  | ||||||
|     ) |  | ||||||
|     raft.quaternion.rotateTowards(quat, delta * 0.5) |  | ||||||
|  |  | ||||||
|     if ( sunLight.visible ) { |  | ||||||
|  |  | ||||||
|     sunLight.position.copy(sun).add(camera.position) |     sunLight.position.copy(sun).add(camera.position) | ||||||
|  |  | ||||||
|     } |     ocean.material.uniforms['time'].value += delta; | ||||||
|  |     updateRaft(delta); | ||||||
|     if ( torchLight.visible ) { |  | ||||||
|      |  | ||||||
|         torchLight.position.copy(camera.position) |  | ||||||
|         torchLight.position.y -= .2 |  | ||||||
|         const targetDirection = camera.getWorldDirection(camera.up).add(camera.position) |  | ||||||
|         torchLight.target.position.copy(targetDirection) |  | ||||||
|      |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderer.render(scene, camera); |     renderer.render(scene, camera); | ||||||
|  |  | ||||||
|     if (showStats) stats.update(); |     if (showStats) stats.update(); | ||||||
|  |  | ||||||
|     requestAnimationFrame( animate ); |  | ||||||
|  |  | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user