main.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
  2. import {player} from './player.js';
  3. import {world} from './world.js';
  4. import {background} from './background.js';
  5. const _VS = `
  6. varying vec3 vWorldPosition;
  7. void main() {
  8. vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
  9. vWorldPosition = worldPosition.xyz;
  10. gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  11. }`;
  12. const _FS = `
  13. uniform vec3 topColor;
  14. uniform vec3 bottomColor;
  15. uniform float offset;
  16. uniform float exponent;
  17. varying vec3 vWorldPosition;
  18. void main() {
  19. float h = normalize( vWorldPosition + offset ).y;
  20. gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 );
  21. }`;
  22. const _PCSS = `
  23. #define LIGHT_WORLD_SIZE 0.05
  24. #define LIGHT_FRUSTUM_WIDTH 3.75
  25. #define LIGHT_SIZE_UV (LIGHT_WORLD_SIZE / LIGHT_FRUSTUM_WIDTH)
  26. #define NEAR_PLANE 1.0
  27. #define NUM_SAMPLES 17
  28. #define NUM_RINGS 11
  29. #define BLOCKER_SEARCH_NUM_SAMPLES NUM_SAMPLES
  30. #define PCF_NUM_SAMPLES NUM_SAMPLES
  31. vec2 poissonDisk[NUM_SAMPLES];
  32. void initPoissonSamples( const in vec2 randomSeed ) {
  33. float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
  34. float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
  35. // jsfiddle that shows sample pattern: https://jsfiddle.net/a16ff1p7/
  36. float angle = rand( randomSeed ) * PI2;
  37. float radius = INV_NUM_SAMPLES;
  38. float radiusStep = radius;
  39. for( int i = 0; i < NUM_SAMPLES; i ++ ) {
  40. poissonDisk[i] = vec2( cos( angle ), sin( angle ) ) * pow( radius, 0.75 );
  41. radius += radiusStep;
  42. angle += ANGLE_STEP;
  43. }
  44. }
  45. float penumbraSize( const in float zReceiver, const in float zBlocker ) { // Parallel plane estimation
  46. return (zReceiver - zBlocker) / zBlocker;
  47. }
  48. float findBlocker( sampler2D shadowMap, const in vec2 uv, const in float zReceiver ) {
  49. // This uses similar triangles to compute what
  50. // area of the shadow map we should search
  51. float searchRadius = LIGHT_SIZE_UV * ( zReceiver - NEAR_PLANE ) / zReceiver;
  52. float blockerDepthSum = 0.0;
  53. int numBlockers = 0;
  54. for( int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; i++ ) {
  55. float shadowMapDepth = unpackRGBAToDepth(texture2D(shadowMap, uv + poissonDisk[i] * searchRadius));
  56. if ( shadowMapDepth < zReceiver ) {
  57. blockerDepthSum += shadowMapDepth;
  58. numBlockers ++;
  59. }
  60. }
  61. if( numBlockers == 0 ) return -1.0;
  62. return blockerDepthSum / float( numBlockers );
  63. }
  64. float PCF_Filter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius ) {
  65. float sum = 0.0;
  66. for( int i = 0; i < PCF_NUM_SAMPLES; i ++ ) {
  67. float depth = unpackRGBAToDepth( texture2D( shadowMap, uv + poissonDisk[ i ] * filterRadius ) );
  68. if( zReceiver <= depth ) sum += 1.0;
  69. }
  70. for( int i = 0; i < PCF_NUM_SAMPLES; i ++ ) {
  71. float depth = unpackRGBAToDepth( texture2D( shadowMap, uv + -poissonDisk[ i ].yx * filterRadius ) );
  72. if( zReceiver <= depth ) sum += 1.0;
  73. }
  74. return sum / ( 2.0 * float( PCF_NUM_SAMPLES ) );
  75. }
  76. float PCSS ( sampler2D shadowMap, vec4 coords ) {
  77. vec2 uv = coords.xy;
  78. float zReceiver = coords.z; // Assumed to be eye-space z in this code
  79. initPoissonSamples( uv );
  80. // STEP 1: blocker search
  81. float avgBlockerDepth = findBlocker( shadowMap, uv, zReceiver );
  82. //There are no occluders so early out (this saves filtering)
  83. if( avgBlockerDepth == -1.0 ) return 1.0;
  84. // STEP 2: penumbra size
  85. float penumbraRatio = penumbraSize( zReceiver, avgBlockerDepth );
  86. float filterRadius = penumbraRatio * LIGHT_SIZE_UV * NEAR_PLANE / zReceiver;
  87. // STEP 3: filtering
  88. //return avgBlockerDepth;
  89. return PCF_Filter( shadowMap, uv, zReceiver, filterRadius );
  90. }
  91. `;
  92. const _PCSSGetShadow = `
  93. return PCSS( shadowMap, shadowCoord );
  94. `;
  95. class BasicWorldDemo {
  96. constructor() {
  97. this._Initialize();
  98. this._gameStarted = false;
  99. document.getElementById('game-menu').onclick = (msg) => this._OnStart(msg);
  100. }
  101. _OnStart(msg) {
  102. document.getElementById('game-menu').style.display = 'none';
  103. this._gameStarted = true;
  104. }
  105. _Initialize() {
  106. // overwrite shadowmap code
  107. let shadowCode = THREE.ShaderChunk.shadowmap_pars_fragment;
  108. shadowCode = shadowCode.replace(
  109. '#ifdef USE_SHADOWMAP',
  110. '#ifdef USE_SHADOWMAP' +
  111. _PCSS
  112. );
  113. shadowCode = shadowCode.replace(
  114. '#if defined( SHADOWMAP_TYPE_PCF )',
  115. _PCSSGetShadow +
  116. '#if defined( SHADOWMAP_TYPE_PCF )'
  117. );
  118. THREE.ShaderChunk.shadowmap_pars_fragment = shadowCode;
  119. // renderer
  120. this.threejs_ = new THREE.WebGLRenderer({
  121. antialias: true,
  122. });
  123. this.threejs_.outputEncoding = THREE.sRGBEncoding;
  124. this.threejs_.gammaFactor = 2.2;
  125. // this.threejs_.toneMapping = THREE.ReinhardToneMapping;
  126. this.threejs_.shadowMap.enabled = true;
  127. // this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
  128. this.threejs_.setPixelRatio(window.devicePixelRatio);
  129. this.threejs_.setSize(window.innerWidth, window.innerHeight);
  130. document.getElementById('container').appendChild(this.threejs_.domElement);
  131. window.addEventListener('resize', () => {
  132. this.OnWindowResize_();
  133. }, false);
  134. const fov = 60;
  135. const aspect = 1920 / 1080;
  136. const near = 1.0;
  137. const far = 20000.0;
  138. this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
  139. this.camera_.position.set(-5, 5, 10);
  140. this.camera_.lookAt(8, 3, 0);
  141. this.scene_ = new THREE.Scene();
  142. let light = new THREE.DirectionalLight(0xFFFFFF, 1.0);
  143. light.position.set(60, 100, 10);
  144. light.target.position.set(40, 0, 0);
  145. light.castShadow = true;
  146. light.shadow.bias = -0.001;
  147. light.shadow.mapSize.width = 4096;
  148. light.shadow.mapSize.height = 4096;
  149. light.shadow.camera.far = 200.0;
  150. light.shadow.camera.near = 1.0;
  151. light.shadow.camera.left = 50;
  152. light.shadow.camera.right = -50;
  153. light.shadow.camera.top = 50;
  154. light.shadow.camera.bottom = -50;
  155. this.scene_.add(light);
  156. light = new THREE.HemisphereLight(0x202020, 0x004080, 0.6);
  157. this.scene_.add(light);
  158. this.scene_.background = new THREE.Color(0x808080);
  159. this.scene_.fog = new THREE.FogExp2(0x89b2eb, 0.00125);
  160. const ground = new THREE.Mesh(
  161. new THREE.PlaneGeometry(20000, 20000, 10, 10),
  162. new THREE.MeshStandardMaterial({
  163. color: 0xf6f47f,
  164. }));
  165. ground.castShadow = false;
  166. ground.receiveShadow = true;
  167. ground.rotation.x = -Math.PI / 2;
  168. this.scene_.add(ground);
  169. const uniforms = {
  170. topColor: { value: new THREE.Color(0x0077FF) },
  171. bottomColor: { value: new THREE.Color(0x89b2eb) },
  172. offset: { value: 33 },
  173. exponent: { value: 0.6 }
  174. };
  175. const skyGeo = new THREE.SphereBufferGeometry(1000, 32, 15);
  176. const skyMat = new THREE.ShaderMaterial({
  177. uniforms: uniforms,
  178. vertexShader: _VS,
  179. fragmentShader: _FS,
  180. side: THREE.BackSide,
  181. });
  182. this.scene_.add(new THREE.Mesh(skyGeo, skyMat));
  183. this.world_ = new world.WorldManager({scene: this.scene_});
  184. this.player_ = new player.Player({scene: this.scene_, world: this.world_});
  185. this.background_ = new background.Background({scene: this.scene_});
  186. this.gameOver_ = false;
  187. this.previousRAF_ = null;
  188. this.RAF_();
  189. this.OnWindowResize_();
  190. }
  191. OnWindowResize_() {
  192. this.camera_.aspect = window.innerWidth / window.innerHeight;
  193. this.camera_.updateProjectionMatrix();
  194. this.threejs_.setSize(window.innerWidth, window.innerHeight);
  195. }
  196. RAF_() {
  197. requestAnimationFrame((t) => {
  198. if (this.previousRAF_ === null) {
  199. this.previousRAF_ = t;
  200. }
  201. this.RAF_();
  202. this.Step_((t - this.previousRAF_) / 1000.0);
  203. this.threejs_.render(this.scene_, this.camera_);
  204. this.previousRAF_ = t;
  205. });
  206. }
  207. Step_(timeElapsed) {
  208. if (this.gameOver_ || !this._gameStarted) {
  209. return;
  210. }
  211. this.player_.Update(timeElapsed);
  212. this.world_.Update(timeElapsed);
  213. this.background_.Update(timeElapsed);
  214. if (this.player_.gameOver && !this.gameOver_) {
  215. this.gameOver_ = true;
  216. document.getElementById('game-over').classList.toggle('active');
  217. }
  218. }
  219. }
  220. let _APP = null;
  221. window.addEventListener('DOMContentLoaded', () => {
  222. _APP = new BasicWorldDemo();
  223. });