├── .gitignore ├── README.en.md ├── README.md ├── bundler ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── package-lock.json ├── package.json ├── park.jpg ├── src ├── Experience │ ├── Baked.js │ ├── Camera.js │ ├── Experience.js │ ├── Renderer.js │ ├── Resources.js │ ├── Utils │ │ ├── EventEmitter.js │ │ ├── Loader.js │ │ ├── Sizes.js │ │ ├── Stats.js │ │ └── Time.js │ ├── World.js │ ├── assets.js │ └── shaders │ │ └── baked │ │ ├── fragment.glsl │ │ └── vertex.glsl ├── index.html ├── script.js └── style.css ├── static ├── .gitkeep ├── assets │ ├── bakedDay.jpg │ ├── bakedNight.jpg │ ├── city.glb │ └── lightMap.jpg ├── basis │ ├── README.md │ ├── basis_transcoder.js │ └── basis_transcoder.wasm └── draco │ ├── README.md │ ├── draco_decoder.js │ ├── draco_decoder.wasm │ ├── draco_encoder.js │ ├── draco_wasm_wrapper.js │ └── gltf │ ├── draco_decoder.js │ ├── draco_decoder.wasm │ ├── draco_encoder.js │ └── draco_wasm_wrapper.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,node 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | .pnpm-debug.log* 43 | 44 | # Diagnostic reports (https://nodejs.org/api/report.html) 45 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 46 | 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | 53 | # Directory for instrumented libs generated by jscoverage/JSCover 54 | lib-cov 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | *.lcov 59 | 60 | # nyc test coverage 61 | .nyc_output 62 | 63 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 64 | .grunt 65 | 66 | # Bower dependency directory (https://bower.io/) 67 | bower_components 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (https://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directories 76 | node_modules/ 77 | jspm_packages/ 78 | 79 | # Snowpack dependency directory (https://snowpack.dev/) 80 | web_modules/ 81 | 82 | # TypeScript cache 83 | *.tsbuildinfo 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Microbundle cache 92 | .rpt2_cache/ 93 | .rts2_cache_cjs/ 94 | .rts2_cache_es/ 95 | .rts2_cache_umd/ 96 | 97 | # Optional REPL history 98 | .node_repl_history 99 | 100 | # Output of 'npm pack' 101 | *.tgz 102 | 103 | # Yarn Integrity file 104 | .yarn-integrity 105 | 106 | # dotenv environment variables file 107 | .env 108 | .env.test 109 | .env.production 110 | 111 | # parcel-bundler cache (https://parceljs.org/) 112 | .cache 113 | .parcel-cache 114 | 115 | # Next.js build output 116 | .next 117 | out 118 | 119 | # Nuxt.js build / generate output 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | .cache/ 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | # public 128 | 129 | # vuepress build output 130 | .vuepress/dist 131 | 132 | # Serverless directories 133 | .serverless/ 134 | 135 | # FuseBox cache 136 | .fusebox/ 137 | 138 | # DynamoDB Local files 139 | .dynamodb/ 140 | 141 | # TernJS port file 142 | .tern-port 143 | 144 | # Stores VSCode versions used for testing VSCode extensions 145 | .vscode-test 146 | 147 | # yarn v2 148 | .yarn/cache 149 | .yarn/unplugged 150 | .yarn/build-state.yml 151 | .yarn/install-state.gz 152 | .pnp.* 153 | 154 | # End of https://www.toptal.com/developers/gitignore/api/macos,node -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # study-case 2 | 3 | #### Description 4 | three.js、three-meshline、paper.js等案例研究 5 | 6 | #### Software Architecture 7 | Software architecture description 8 | 9 | #### Installation 10 | 11 | 1. xxxx 12 | 2. xxxx 13 | 3. xxxx 14 | 15 | #### Instructions 16 | 17 | 1. xxxx 18 | 2. xxxx 19 | 3. xxxx 20 | 21 | #### Contribution 22 | 23 | 1. Fork the repository 24 | 2. Create Feat_xxx branch 25 | 3. Commit your code 26 | 4. Create Pull Request 27 | 28 | 29 | #### Gitee Feature 30 | 31 | 1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md 32 | 2. Gitee blog [blog.gitee.com](https://blog.gitee.com) 33 | 3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) 34 | 4. The most valuable open source project [GVP](https://gitee.com/gvp) 35 | 5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) 36 | 6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 智慧园区 2 | 3 | [Live Demo](https://gcat.cc/demo/park) 4 | 5 |  6 | 7 | ## Setup 8 | 9 | Download [Node.js](https://nodejs.org/en/download/). 10 | Run this followed commands: 11 | 12 | ```bash 13 | # Install dependencies (only the first time) 14 | npm install 15 | 16 | # Run the local server at localhost:8080 17 | npm run dev 18 | 19 | # Build for production in the dist/ directory 20 | npm run build 21 | ``` 22 | -------------------------------------------------------------------------------- /bundler/webpack.common.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const MiniCSSExtractPlugin = require('mini-css-extract-plugin') 4 | const path = require('path') 5 | 6 | module.exports = { 7 | entry: path.resolve(__dirname, '../src/script.js'), 8 | output: 9 | { 10 | filename: 'bundle.[contenthash].js', 11 | path: path.resolve(__dirname, '../dist') 12 | }, 13 | devtool: 'source-map', 14 | plugins: 15 | [ 16 | new CopyWebpackPlugin({ 17 | patterns: [ 18 | { from: path.resolve(__dirname, '../static') } 19 | ] 20 | }), 21 | new HtmlWebpackPlugin({ 22 | template: path.resolve(__dirname, '../src/index.html'), 23 | minify: true 24 | }), 25 | new MiniCSSExtractPlugin() 26 | ], 27 | module: 28 | { 29 | rules: 30 | [ 31 | // HTML 32 | { 33 | test: /\.(html)$/, 34 | use: ['html-loader'] 35 | }, 36 | 37 | // JS 38 | { 39 | test: /\.js$/, 40 | exclude: /node_modules/, 41 | use: 42 | [ 43 | 'babel-loader' 44 | ] 45 | }, 46 | 47 | // CSS 48 | { 49 | test: /\.css$/, 50 | use: 51 | [ 52 | MiniCSSExtractPlugin.loader, 53 | 'css-loader' 54 | ] 55 | }, 56 | 57 | // Images 58 | { 59 | test: /\.(jpg|png|gif|svg)$/, 60 | use: 61 | [ 62 | { 63 | loader: 'file-loader', 64 | options: 65 | { 66 | outputPath: 'assets/images/' 67 | } 68 | } 69 | ] 70 | }, 71 | 72 | // Fonts 73 | { 74 | test: /\.(ttf|eot|woff|woff2)$/, 75 | use: 76 | [ 77 | { 78 | loader: 'file-loader', 79 | options: 80 | { 81 | outputPath: 'assets/fonts/' 82 | } 83 | } 84 | ] 85 | }, 86 | 87 | // Shaders 88 | { 89 | test: /\.(glsl|vs|fs|vert|frag)$/, 90 | exclude: /node_modules/, 91 | use: [ 92 | 'raw-loader', 93 | 'glslify-loader' 94 | ] 95 | } 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /bundler/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const commonConfiguration = require('./webpack.common.js') 3 | const ip = require('internal-ip') 4 | const portFinderSync = require('portfinder-sync') 5 | 6 | const infoColor = (_message) => 7 | { 8 | return `\u001b[1m\u001b[34m${_message}\u001b[39m\u001b[22m` 9 | } 10 | 11 | module.exports = merge( 12 | commonConfiguration, 13 | { 14 | mode: 'development', 15 | devServer: 16 | { 17 | host: '0.0.0.0', 18 | port: portFinderSync.getPort(8080), 19 | contentBase: './dist', 20 | watchContentBase: true, 21 | open: true, 22 | https: false, 23 | useLocalIp: true, 24 | disableHostCheck: true, 25 | overlay: true, 26 | noInfo: true, 27 | after: function(app, server, compiler) 28 | { 29 | const port = server.options.port 30 | const https = server.options.https ? 's' : '' 31 | const localIp = ip.v4.sync() 32 | const domain1 = `http${https}://${localIp}:${port}` 33 | const domain2 = `http${https}://localhost:${port}` 34 | 35 | console.log(`Project running at:\n - ${infoColor(domain1)}\n - ${infoColor(domain2)}`) 36 | } 37 | } 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /bundler/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const commonConfiguration = require('./webpack.common.js') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = merge( 6 | commonConfiguration, 7 | { 8 | mode: 'production', 9 | plugins: 10 | [ 11 | new CleanWebpackPlugin() 12 | ] 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "#", 3 | "license": "UNLICENSED", 4 | "scripts": { 5 | "build": "webpack --config ./bundler/webpack.prod.js", 6 | "dev": "webpack serve --config ./bundler/webpack.dev.js", 7 | "prod": "git switch prod; git merge main; git push; git switch main" 8 | }, 9 | "dependencies": { 10 | "@babel/core": "^7.14.6", 11 | "@babel/preset-env": "^7.14.7", 12 | "babel-loader": "^8.2.2", 13 | "clean-webpack-plugin": "^3.0.0", 14 | "copy-webpack-plugin": "^9.0.1", 15 | "css-loader": "^5.2.6", 16 | "file-loader": "^6.2.0", 17 | "glsl-blend": "^1.0.3", 18 | "glslify-loader": "^2.0.0", 19 | "gsap": "^3.7.1", 20 | "html-loader": "^2.1.2", 21 | "html-webpack-plugin": "^5.3.2", 22 | "mini-css-extract-plugin": "^2.1.0", 23 | "normalize-wheel": "^1.0.1", 24 | "portfinder-sync": "0.0.2", 25 | "raw-loader": "^4.0.2", 26 | "stats.js": "^0.17.0", 27 | "style-loader": "^3.0.0", 28 | "three": "^0.130.1", 29 | "tweakpane": "^3.0.5", 30 | "webpack": "^5.42.1", 31 | "webpack-cli": "^4.7.2", 32 | "webpack-dev-server": "^3.11.2", 33 | "webpack-merge": "^5.8.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /park.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostCatcg/day-night-digital-twins/fd4caeb66a2226d681474f230215a836a72774a7/park.jpg -------------------------------------------------------------------------------- /src/Experience/Baked.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import Experience from "./Experience.js"; 4 | import vertexShader from "./shaders/baked/vertex.glsl"; 5 | import fragmentShader from "./shaders/baked/fragment.glsl"; 6 | import gsap from "gsap"; 7 | export default class Baked { 8 | constructor() { 9 | this.experience = new Experience(); 10 | this.resources = this.experience.resources; 11 | this.debug = this.experience.debug; 12 | this.scene = this.experience.scene; 13 | this.time = this.experience.time; 14 | 15 | // Debug 16 | if (this.debug) { 17 | this.debugFolder = this.debug.addFolder({ 18 | title: "智慧园区", 19 | expanded: true, 20 | }); 21 | } 22 | 23 | this.setModel(); 24 | } 25 | 26 | setModel() { 27 | this.model = {}; 28 | 29 | this.model.mesh = this.resources.items.roomModel.scene.children[0]; 30 | 31 | this.model.bakedDayTexture = this.resources.items.bakedDayTexture; 32 | this.model.bakedDayTexture.encoding = THREE.sRGBEncoding; 33 | this.model.bakedDayTexture.flipY = false; 34 | 35 | this.model.bakedNightTexture = this.resources.items.bakedNightTexture; 36 | this.model.bakedNightTexture.encoding = THREE.sRGBEncoding; 37 | this.model.bakedNightTexture.flipY = false; 38 | 39 | this.model.lightMapTexture = this.resources.items.lightMapTexture; 40 | this.model.lightMapTexture.encoding = THREE.sRGBEncoding; 41 | this.model.lightMapTexture.flipY = false; 42 | 43 | this.colors = {}; 44 | this.colors.window = "#ffffff"; 45 | this.colors.lamp = "#ffffff"; 46 | this.colors.other = "#ffffff"; 47 | 48 | this.model.material = new THREE.ShaderMaterial({ 49 | uniforms: { 50 | uBakedDayTexture: { value: this.model.bakedDayTexture }, 51 | uBakedNightTexture: { value: this.model.bakedNightTexture }, 52 | // uBakedNeutralTexture: { value: this.model.bakedNeutralTexture }, 53 | uLightMapTexture: { value: this.model.lightMapTexture }, 54 | 55 | uNightMix: { value: 0 }, 56 | uNeutralMix: { value: 0 }, 57 | 58 | uLightWindowColor: { value: new THREE.Color(this.colors.window) }, 59 | uLightWindowStrength: { value: 0 }, 60 | 61 | uLightLamOtherolor: { value: new THREE.Color(this.colors.lamp) }, 62 | uLightLampStrength: { value: 0 }, 63 | 64 | uLightOtherColor: { value: new THREE.Color(this.colors.other) }, 65 | uLightOtherStrength: { value: 0 }, 66 | }, 67 | vertexShader: vertexShader, 68 | fragmentShader: fragmentShader, 69 | }); 70 | 71 | this.model.mesh.traverse((_child) => { 72 | if (_child instanceof THREE.Mesh) { 73 | _child.material = this.model.material; 74 | } 75 | }); 76 | 77 | this.scene.add(this.model.mesh); 78 | 79 | // Debug 80 | if (this.debug) { 81 | const btn = this.debugFolder.addButton({ 82 | title: "切换", 83 | label: "白天/夜晚", 84 | }); 85 | 86 | // this.debugFolder.addInput( 87 | // this.model.material.uniforms.uNightMix, 88 | // "value", 89 | // { label: "白天/夜晚", min: 0, max: 1 } 90 | // ); 91 | 92 | btn.on("click", () => { 93 | const number = 94 | this.model.material.uniforms.uNightMix.value === 1 ? 0 : 1; 95 | gsap.to(this.model.material.uniforms.uNightMix, { 96 | duration: 1, 97 | value: number, 98 | }); 99 | }); 100 | 101 | this.debugFolder 102 | .addInput(this.colors, "window", { view: "color" }) 103 | .on("change", () => { 104 | this.model.material.uniforms.uLightWindowColor.value.set( 105 | this.colors.window 106 | ); 107 | }); 108 | 109 | this.debugFolder.addInput( 110 | this.model.material.uniforms.uLightWindowStrength, 111 | "value", 112 | { label: "窗户灯光强度", min: 0, max: 1 } 113 | ); 114 | 115 | this.debugFolder 116 | .addInput(this.colors, "lamp", { view: "color" }) 117 | .on("change", () => { 118 | this.model.material.uniforms.uLightLamOtherolor.value.set( 119 | this.colors.lamp 120 | ); 121 | }); 122 | 123 | this.debugFolder.addInput( 124 | this.model.material.uniforms.uLightLampStrength, 125 | "value", 126 | { label: "路灯灯光强度", min: 0, max: 3 } 127 | ); 128 | 129 | this.debugFolder 130 | .addInput(this.colors, "other", { view: "color" }) 131 | .on("change", () => { 132 | this.model.material.uniforms.uLightOtherColor.value.set( 133 | this.colors.other 134 | ); 135 | }); 136 | 137 | this.debugFolder.addInput( 138 | this.model.material.uniforms.uLightOtherStrength, 139 | "value", 140 | { label: "装饰灯光强度", min: 0, max: 3 } 141 | ); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Experience/Camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | 4 | export default class Camera { 5 | constructor(_options) { 6 | // Options 7 | this.experience = new Experience(); 8 | this.config = this.experience.config; 9 | this.debug = this.experience.debug; 10 | this.scene = this.experience.scene; 11 | 12 | // Set up 13 | this.mode = "default"; // default \ debug 14 | 15 | this.setInstance(); 16 | } 17 | 18 | setInstance() { 19 | // Set up 20 | this.instance = new THREE.PerspectiveCamera( 21 | 45, 22 | this.config.width / this.config.height, 23 | 0.1, 24 | 15000 25 | ); 26 | this.instance.position.set(-2.7,76,151) 27 | this.scene.add(this.instance); 28 | } 29 | 30 | resize() { 31 | this.instance.aspect = this.config.width / this.config.height; 32 | this.instance.updateProjectionMatrix(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Experience/Experience.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | import { Pane } from "tweakpane"; 4 | 5 | import Time from "./Utils/Time.js"; 6 | import Sizes from "./Utils/Sizes.js"; 7 | import Stats from "./Utils/Stats.js"; 8 | 9 | import Resources from "./Resources.js"; 10 | import Renderer from "./Renderer.js"; 11 | import Camera from "./Camera.js"; 12 | import World from "./World.js"; 13 | 14 | import assets from "./assets.js"; 15 | 16 | export default class Experience { 17 | static instance; 18 | 19 | constructor(_options = {}) { 20 | // 这一步骤是关键,可是实现一个实例对象 21 | if (Experience.instance) { 22 | return Experience.instance; 23 | } 24 | Experience.instance = this; 25 | 26 | // Options 27 | this.targetElement = _options.targetElement; 28 | 29 | if (!this.targetElement) { 30 | console.warn("Missing 'targetElement' property"); 31 | return; 32 | } 33 | 34 | this.time = new Time(); 35 | this.sizes = new Sizes(); 36 | this.setConfig(); // 设置配置 37 | this.setStats(); // 设置帧速率 38 | this.setDebug(); // 设置debug ui 39 | this.setScene(); // 设置场景 40 | this.setCamera(); // 设置相机 41 | this.setRenderer(); // 设置渲染器 42 | this.setResources(); // 设置资源 43 | this.setWorld(); // 设置世界 44 | this.setControls(); // 控制器 45 | 46 | // window.resize 47 | this.sizes.on("resize", () => { 48 | this.resize(); 49 | }); 50 | 51 | this.resources.on("groupEnd", () => { 52 | document.querySelector(".loading").style.display = "none"; 53 | this.resize(); 54 | }); 55 | 56 | this.update(); // 更新渲染 57 | } 58 | static getInstance(_options = {}) { 59 | if (Experience.instance) { 60 | return Experience.instance; 61 | } 62 | 63 | Experience.instance = new Experience(_options); 64 | 65 | return Experience.instance; 66 | } 67 | 68 | setConfig() { 69 | this.config = {}; 70 | 71 | // Pixel ratio 72 | this.config.pixelRatio = Math.min(Math.max(window.devicePixelRatio, 1), 2); 73 | 74 | // Width and height 75 | const boundings = this.targetElement.getBoundingClientRect(); 76 | this.config.width = boundings.width; 77 | this.config.height = boundings.height || window.innerHeight; 78 | this.config.smallestSide = Math.min(this.config.width, this.config.height); 79 | this.config.largestSide = Math.max(this.config.width, this.config.height); 80 | 81 | // Debug 82 | // this.config.debug = window.location.hash === '#debug' 83 | this.config.debug = this.config.width > 420; 84 | } 85 | 86 | setStats() { 87 | if (this.config.debug) { 88 | this.stats = new Stats(true); 89 | } 90 | } 91 | 92 | setDebug() { 93 | if (this.config.debug) { 94 | this.debug = new Pane(); 95 | this.debug.containerElem_.style.width = "320px"; 96 | } 97 | } 98 | 99 | setScene() { 100 | this.scene = new THREE.Scene(); 101 | } 102 | 103 | setCamera() { 104 | this.camera = new Camera(); 105 | } 106 | 107 | setRenderer() { 108 | this.renderer = new Renderer({ rendererInstance: this.rendererInstance }); 109 | 110 | // 添加到dom 111 | this.targetElement.appendChild(this.renderer.instance.domElement); 112 | } 113 | 114 | setControls() { 115 | this.controls = new OrbitControls( 116 | this.camera.instance, 117 | this.renderer.instance.domElement 118 | ); 119 | this.controls.enableDamping = true; 120 | this.controls.maxDistance = 400; 121 | this.controls.minDistance = 4; 122 | this.controls.update(); 123 | } 124 | 125 | setResources() { 126 | this.resources = new Resources(assets); 127 | } 128 | 129 | setWorld() { 130 | this.world = new World(); 131 | } 132 | 133 | update() { 134 | this.controls.update(); 135 | if (this.stats) this.stats.update(); 136 | 137 | if (this.renderer) this.renderer.update(); 138 | 139 | window.requestAnimationFrame(() => { 140 | this.update(); 141 | }); 142 | } 143 | 144 | resize() { 145 | // Config 146 | const boundings = this.targetElement.getBoundingClientRect(); 147 | this.config.width = boundings.width; 148 | this.config.height = boundings.height; 149 | this.config.smallestSide = Math.min(this.config.width, this.config.height); 150 | this.config.largestSide = Math.max(this.config.width, this.config.height); 151 | 152 | this.config.pixelRatio = Math.min(Math.max(window.devicePixelRatio, 1), 2); 153 | 154 | if (this.camera) this.camera.resize(); 155 | 156 | if (this.renderer) this.renderer.resize(); 157 | } 158 | 159 | destroy() {} 160 | } 161 | -------------------------------------------------------------------------------- /src/Experience/Renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Experience from './Experience.js' 3 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' 4 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' 5 | 6 | export default class Renderer 7 | { 8 | constructor(_options = {}) 9 | { 10 | this.experience = new Experience() 11 | this.config = this.experience.config 12 | this.debug = this.experience.debug 13 | this.stats = this.experience.stats 14 | this.time = this.experience.time 15 | this.sizes = this.experience.sizes 16 | this.scene = this.experience.scene 17 | this.camera = this.experience.camera 18 | 19 | this.usePostprocess = false 20 | 21 | this.setInstance() 22 | this.setPostProcess() 23 | } 24 | 25 | setInstance() 26 | { 27 | this.clearColor = '#010101' 28 | 29 | // Renderer 30 | this.instance = new THREE.WebGLRenderer({ 31 | alpha: false, 32 | antialias: true 33 | }) 34 | this.instance.domElement.style.position = 'absolute' 35 | this.instance.domElement.style.top = 0 36 | this.instance.domElement.style.left = 0 37 | this.instance.domElement.style.width = '100%' 38 | this.instance.domElement.style.height = '100%' 39 | 40 | // this.instance.setClearColor(0x414141, 1) 41 | this.instance.setClearColor(this.clearColor, 1) 42 | this.instance.setSize(this.config.width, this.config.height) 43 | this.instance.setPixelRatio(this.config.pixelRatio) 44 | 45 | // this.instance.physicallyCorrectLights = true 46 | // this.instance.gammaOutPut = true 47 | this.instance.outputEncoding = THREE.sRGBEncoding 48 | // this.instance.shadowMap.type = THREE.PCFSoftShadowMap 49 | // this.instance.shadowMap.enabled = false 50 | // this.instance.toneMapping = THREE.ReinhardToneMapping 51 | // this.instance.toneMappingExposure = 1.3 52 | 53 | this.context = this.instance.getContext() 54 | 55 | // Add stats panel 56 | if(this.stats) 57 | { 58 | this.stats.setRenderPanel(this.context) 59 | } 60 | } 61 | 62 | setPostProcess() 63 | { 64 | this.postProcess = {} 65 | 66 | /** 67 | * Render pass 68 | */ 69 | this.postProcess.renderPass = new RenderPass(this.scene, this.camera.instance) 70 | 71 | /** 72 | * Effect composer 73 | */ 74 | const RenderTargetClass = this.config.pixelRatio >= 2 ? THREE.WebGLRenderTarget : THREE.WebGLMultisampleRenderTarget 75 | // const RenderTargetClass = THREE.WebGLRenderTarget 76 | this.renderTarget = new RenderTargetClass( 77 | this.config.width, 78 | this.config.height, 79 | { 80 | generateMipmaps: false, 81 | minFilter: THREE.LinearFilter, 82 | magFilter: THREE.LinearFilter, 83 | format: THREE.RGBFormat, 84 | encoding: THREE.sRGBEncoding 85 | } 86 | ) 87 | this.postProcess.composer = new EffectComposer(this.instance, this.renderTarget) 88 | this.postProcess.composer.setSize(this.config.width, this.config.height) 89 | this.postProcess.composer.setPixelRatio(this.config.pixelRatio) 90 | 91 | this.postProcess.composer.addPass(this.postProcess.renderPass) 92 | } 93 | 94 | resize() 95 | { 96 | // Instance 97 | this.instance.setSize(this.config.width, this.config.height) 98 | this.instance.setPixelRatio(this.config.pixelRatio) 99 | 100 | // Post process 101 | this.postProcess.composer.setSize(this.config.width, this.config.height) 102 | this.postProcess.composer.setPixelRatio(this.config.pixelRatio) 103 | } 104 | 105 | update() 106 | { 107 | if(this.stats) 108 | { 109 | this.stats.beforeRender() 110 | } 111 | 112 | if(this.usePostprocess) 113 | { 114 | this.postProcess.composer.render() 115 | } 116 | else 117 | { 118 | this.instance.render(this.scene, this.camera.instance) 119 | } 120 | 121 | if(this.stats) 122 | { 123 | this.stats.afterRender() 124 | } 125 | } 126 | 127 | destroy() 128 | { 129 | this.instance.renderLists.dispose() 130 | this.instance.dispose() 131 | this.renderTarget.dispose() 132 | this.postProcess.composer.renderTarget1.dispose() 133 | this.postProcess.composer.renderTarget2.dispose() 134 | } 135 | } -------------------------------------------------------------------------------- /src/Experience/Resources.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import EventEmitter from './Utils/EventEmitter.js' 3 | import Loader from './Utils/Loader.js' 4 | 5 | export default class Resources extends EventEmitter 6 | { 7 | constructor(_assets) 8 | { 9 | super() 10 | 11 | // Items (will contain every resources) 12 | this.items = {} 13 | 14 | // Loader 15 | this.loader = new Loader({ renderer: this.renderer }) 16 | 17 | this.groups = {} 18 | this.groups.assets = [..._assets] 19 | this.groups.loaded = [] 20 | this.groups.current = null 21 | this.loadNextGroup() 22 | 23 | // Loader file end event 24 | this.loader.on('fileEnd', (_resource, _data) => 25 | { 26 | let data = _data 27 | 28 | // Convert to texture 29 | if(_resource.type === 'texture') 30 | { 31 | if(!(data instanceof THREE.Texture)) 32 | { 33 | data = new THREE.Texture(_data) 34 | } 35 | data.needsUpdate = true 36 | } 37 | 38 | this.items[_resource.name] = data 39 | 40 | // Progress and event 41 | this.groups.current.loaded++ 42 | this.trigger('progress', [this.groups.current, _resource, data]) 43 | }) 44 | 45 | // Loader all end event 46 | this.loader.on('end', () => 47 | { 48 | this.groups.loaded.push(this.groups.current) 49 | 50 | // Trigger 51 | this.trigger('groupEnd', [this.groups.current]) 52 | 53 | if(this.groups.assets.length > 0) 54 | { 55 | this.loadNextGroup() 56 | } 57 | else 58 | { 59 | this.trigger('end') 60 | } 61 | }) 62 | } 63 | 64 | loadNextGroup() 65 | { 66 | this.groups.current = this.groups.assets.shift() 67 | this.groups.current.toLoad = this.groups.current.items.length 68 | this.groups.current.loaded = 0 69 | 70 | this.loader.load(this.groups.current.items) 71 | } 72 | 73 | createInstancedMeshes(_children, _groups) 74 | { 75 | // Groups 76 | const groups = [] 77 | 78 | for(const _group of _groups) 79 | { 80 | groups.push({ 81 | name: _group.name, 82 | regex: _group.regex, 83 | meshesGroups: [], 84 | instancedMeshes: [] 85 | }) 86 | } 87 | 88 | // Result 89 | const result = {} 90 | 91 | for(const _group of groups) 92 | { 93 | result[_group.name] = _group.instancedMeshes 94 | } 95 | 96 | return result 97 | } 98 | 99 | destroy() 100 | { 101 | for(const _itemKey in this.items) 102 | { 103 | const item = this.items[_itemKey] 104 | if(item instanceof THREE.Texture) 105 | { 106 | item.dispose() 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Experience/Utils/EventEmitter.js: -------------------------------------------------------------------------------- 1 | export default class 2 | { 3 | /** 4 | * Constructor 5 | */ 6 | constructor() 7 | { 8 | this.callbacks = {} 9 | this.callbacks.base = {} 10 | } 11 | 12 | /** 13 | * On 14 | */ 15 | on(_names, callback) 16 | { 17 | const that = this 18 | 19 | // Errors 20 | if(typeof _names === 'undefined' || _names === '') 21 | { 22 | console.warn('wrong names') 23 | return false 24 | } 25 | 26 | if(typeof callback === 'undefined') 27 | { 28 | console.warn('wrong callback') 29 | return false 30 | } 31 | 32 | // Resolve names 33 | const names = this.resolveNames(_names) 34 | 35 | // Each name 36 | names.forEach(function(_name) 37 | { 38 | // Resolve name 39 | const name = that.resolveName(_name) 40 | 41 | // Create namespace if not exist 42 | if(!(that.callbacks[ name.namespace ] instanceof Object)) 43 | that.callbacks[ name.namespace ] = {} 44 | 45 | // Create callback if not exist 46 | if(!(that.callbacks[ name.namespace ][ name.value ] instanceof Array)) 47 | that.callbacks[ name.namespace ][ name.value ] = [] 48 | 49 | // Add callback 50 | that.callbacks[ name.namespace ][ name.value ].push(callback) 51 | }) 52 | 53 | return this 54 | } 55 | 56 | /** 57 | * Off 58 | */ 59 | off(_names) 60 | { 61 | const that = this 62 | 63 | // Errors 64 | if(typeof _names === 'undefined' || _names === '') 65 | { 66 | console.warn('wrong name') 67 | return false 68 | } 69 | 70 | // Resolve names 71 | const names = this.resolveNames(_names) 72 | 73 | // Each name 74 | names.forEach(function(_name) 75 | { 76 | // Resolve name 77 | const name = that.resolveName(_name) 78 | 79 | // Remove namespace 80 | if(name.namespace !== 'base' && name.value === '') 81 | { 82 | delete that.callbacks[ name.namespace ] 83 | } 84 | 85 | // Remove specific callback in namespace 86 | else 87 | { 88 | // Default 89 | if(name.namespace === 'base') 90 | { 91 | // Try to remove from each namespace 92 | for(const namespace in that.callbacks) 93 | { 94 | if(that.callbacks[ namespace ] instanceof Object && that.callbacks[ namespace ][ name.value ] instanceof Array) 95 | { 96 | delete that.callbacks[ namespace ][ name.value ] 97 | 98 | // Remove namespace if empty 99 | if(Object.keys(that.callbacks[ namespace ]).length === 0) 100 | delete that.callbacks[ namespace ] 101 | } 102 | } 103 | } 104 | 105 | // Specified namespace 106 | else if(that.callbacks[ name.namespace ] instanceof Object && that.callbacks[ name.namespace ][ name.value ] instanceof Array) 107 | { 108 | delete that.callbacks[ name.namespace ][ name.value ] 109 | 110 | // Remove namespace if empty 111 | if(Object.keys(that.callbacks[ name.namespace ]).length === 0) 112 | delete that.callbacks[ name.namespace ] 113 | } 114 | } 115 | }) 116 | 117 | return this 118 | } 119 | 120 | /** 121 | * Trigger 122 | */ 123 | trigger(_name, _args) 124 | { 125 | // Errors 126 | if(typeof _name === 'undefined' || _name === '') 127 | { 128 | console.warn('wrong name') 129 | return false 130 | } 131 | 132 | const that = this 133 | let finalResult = null 134 | let result = null 135 | 136 | // Default args 137 | const args = !(_args instanceof Array) ? [] : _args 138 | 139 | // Resolve names (should on have one event) 140 | let name = this.resolveNames(_name) 141 | 142 | // Resolve name 143 | name = this.resolveName(name[ 0 ]) 144 | 145 | // Default namespace 146 | if(name.namespace === 'base') 147 | { 148 | // Try to find callback in each namespace 149 | for(const namespace in that.callbacks) 150 | { 151 | if(that.callbacks[ namespace ] instanceof Object && that.callbacks[ namespace ][ name.value ] instanceof Array) 152 | { 153 | that.callbacks[ namespace ][ name.value ].forEach(function(callback) 154 | { 155 | result = callback.apply(that, args) 156 | 157 | if(typeof finalResult === 'undefined') 158 | { 159 | finalResult = result 160 | } 161 | }) 162 | } 163 | } 164 | } 165 | 166 | // Specified namespace 167 | else if(this.callbacks[ name.namespace ] instanceof Object) 168 | { 169 | if(name.value === '') 170 | { 171 | console.warn('wrong name') 172 | return this 173 | } 174 | 175 | that.callbacks[ name.namespace ][ name.value ].forEach(function(callback) 176 | { 177 | result = callback.apply(that, args) 178 | 179 | if(typeof finalResult === 'undefined') 180 | finalResult = result 181 | }) 182 | } 183 | 184 | return finalResult 185 | } 186 | 187 | /** 188 | * Resolve names 189 | */ 190 | resolveNames(_names) 191 | { 192 | let names = _names 193 | names = names.replace(/[^a-zA-Z0-9 ,/.]/g, '') 194 | names = names.replace(/[,/]+/g, ' ') 195 | names = names.split(' ') 196 | 197 | return names 198 | } 199 | 200 | /** 201 | * Resolve name 202 | */ 203 | resolveName(name) 204 | { 205 | const newName = {} 206 | const parts = name.split('.') 207 | 208 | newName.original = name 209 | newName.value = parts[ 0 ] 210 | newName.namespace = 'base' // Base namespace 211 | 212 | // Specified namespace 213 | if(parts.length > 1 && parts[ 1 ] !== '') 214 | { 215 | newName.namespace = parts[ 1 ] 216 | } 217 | 218 | return newName 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Experience/Utils/Loader.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter.js' 2 | import Experience from '../Experience.js' 3 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' 4 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js' 5 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' 6 | import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' 7 | import { BasisTextureLoader } from 'three/examples/jsm/loaders/BasisTextureLoader.js' 8 | 9 | export default class Resources extends EventEmitter 10 | { 11 | /** 12 | * Constructor 13 | */ 14 | constructor() 15 | { 16 | super() 17 | 18 | this.experience = new Experience() 19 | this.renderer = this.experience.renderer.instance 20 | 21 | this.setLoaders() 22 | 23 | this.toLoad = 0 24 | this.loaded = 0 25 | this.items = {} 26 | } 27 | 28 | /** 29 | * Set loaders 30 | */ 31 | setLoaders() 32 | { 33 | this.loaders = [] 34 | 35 | // Images 36 | this.loaders.push({ 37 | extensions: ['jpg', 'png'], 38 | action: (_resource) => 39 | { 40 | const image = new Image() 41 | 42 | image.addEventListener('load', () => 43 | { 44 | this.fileLoadEnd(_resource, image) 45 | }) 46 | 47 | image.addEventListener('error', () => 48 | { 49 | this.fileLoadEnd(_resource, image) 50 | }) 51 | 52 | image.src = _resource.source 53 | } 54 | }) 55 | 56 | // Basis images 57 | const basisLoader = new BasisTextureLoader() 58 | basisLoader.setTranscoderPath('basis') 59 | basisLoader.detectSupport(this.renderer) 60 | 61 | this.loaders.push({ 62 | extensions: ['basis'], 63 | action: (_resource) => 64 | { 65 | basisLoader.load(_resource.source, (_data) => 66 | { 67 | this.fileLoadEnd(_resource, _data) 68 | }) 69 | } 70 | }) 71 | 72 | // Draco 73 | const dracoLoader = new DRACOLoader() 74 | dracoLoader.setDecoderPath('draco/') 75 | dracoLoader.setDecoderConfig({ type: 'js' }) 76 | 77 | this.loaders.push({ 78 | extensions: ['drc'], 79 | action: (_resource) => 80 | { 81 | dracoLoader.load(_resource.source, (_data) => 82 | { 83 | this.fileLoadEnd(_resource, _data) 84 | 85 | DRACOLoader.releaseDecoderModule() 86 | }) 87 | } 88 | }) 89 | 90 | // GLTF 91 | const gltfLoader = new GLTFLoader() 92 | gltfLoader.setDRACOLoader(dracoLoader) 93 | 94 | this.loaders.push({ 95 | extensions: ['glb', 'gltf'], 96 | action: (_resource) => 97 | { 98 | gltfLoader.load(_resource.source, (_data) => 99 | { 100 | this.fileLoadEnd(_resource, _data) 101 | }) 102 | } 103 | }) 104 | 105 | // FBX 106 | const fbxLoader = new FBXLoader() 107 | 108 | this.loaders.push({ 109 | extensions: ['fbx'], 110 | action: (_resource) => 111 | { 112 | fbxLoader.load(_resource.source, (_data) => 113 | { 114 | this.fileLoadEnd(_resource, _data) 115 | }) 116 | } 117 | }) 118 | 119 | // RGBE | HDR 120 | const rgbeLoader = new RGBELoader() 121 | 122 | this.loaders.push({ 123 | extensions: ['hdr'], 124 | action: (_resource) => 125 | { 126 | rgbeLoader.load(_resource.source, (_data) => 127 | { 128 | this.fileLoadEnd(_resource, _data) 129 | }) 130 | } 131 | }) 132 | } 133 | 134 | /** 135 | * Load 136 | */ 137 | load(_resources = []) 138 | { 139 | for(const _resource of _resources) 140 | { 141 | this.toLoad++ 142 | const extensionMatch = _resource.source.match(/\.([a-z]+)$/) 143 | 144 | if(typeof extensionMatch[1] !== 'undefined') 145 | { 146 | const extension = extensionMatch[1] 147 | const loader = this.loaders.find((_loader) => _loader.extensions.find((_extension) => _extension === extension)) 148 | 149 | if(loader) 150 | { 151 | loader.action(_resource) 152 | } 153 | else 154 | { 155 | console.warn(`Cannot found loader for ${_resource}`) 156 | } 157 | } 158 | else 159 | { 160 | console.warn(`Cannot found extension of ${_resource}`) 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * File load end 167 | */ 168 | fileLoadEnd(_resource, _data) 169 | { 170 | this.loaded++ 171 | this.items[_resource.name] = _data 172 | 173 | this.trigger('fileEnd', [_resource, _data]) 174 | 175 | if(this.loaded === this.toLoad) 176 | { 177 | this.trigger('end') 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Experience/Utils/Sizes.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter.js' 2 | 3 | export default class Sizes extends EventEmitter 4 | { 5 | /** 6 | * Constructor 7 | */ 8 | constructor() 9 | { 10 | super() 11 | 12 | // Viewport size 13 | this.viewport = {} 14 | this.$sizeViewport = document.createElement('div') 15 | this.$sizeViewport.style.width = '100vw' 16 | this.$sizeViewport.style.height = '100vh' 17 | this.$sizeViewport.style.position = 'absolute' 18 | this.$sizeViewport.style.top = 0 19 | this.$sizeViewport.style.left = 0 20 | this.$sizeViewport.style.pointerEvents = 'none' 21 | 22 | // Resize event 23 | this.resize = this.resize.bind(this) 24 | window.addEventListener('resize', this.resize) 25 | 26 | this.resize() 27 | } 28 | 29 | /** 30 | * Resize 31 | */ 32 | resize() 33 | { 34 | document.body.appendChild(this.$sizeViewport) 35 | this.viewport.width = this.$sizeViewport.offsetWidth 36 | this.viewport.height = this.$sizeViewport.offsetHeight 37 | document.body.removeChild(this.$sizeViewport) 38 | 39 | this.width = window.innerWidth 40 | this.height = window.innerHeight 41 | 42 | this.trigger('resize') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Experience/Utils/Stats.js: -------------------------------------------------------------------------------- 1 | import StatsJs from 'stats.js' 2 | 3 | export default class Stats 4 | { 5 | constructor(_active) 6 | { 7 | this.instance = new StatsJs() 8 | this.instance.showPanel(3) 9 | 10 | this.active = false 11 | this.max = 40 12 | this.ignoreMaxed = true 13 | 14 | if(_active) 15 | { 16 | this.activate() 17 | } 18 | } 19 | 20 | activate() 21 | { 22 | this.active = true 23 | 24 | document.body.appendChild(this.instance.dom) 25 | } 26 | 27 | deactivate() 28 | { 29 | this.active = false 30 | 31 | document.body.removeChild(this.instance.dom) 32 | } 33 | 34 | setRenderPanel(_context) 35 | { 36 | this.render = {} 37 | this.render.context = _context 38 | this.render.extension = this.render.context.getExtension('EXT_disjoint_timer_query_webgl2') 39 | this.render.panel = this.instance.addPanel(new StatsJs.Panel('Render (ms)', '#f8f', '#212')) 40 | 41 | const webGL2 = typeof WebGL2RenderingContext !== 'undefined' && _context instanceof WebGL2RenderingContext 42 | 43 | if(!webGL2 || !this.render.extension) 44 | { 45 | this.deactivate() 46 | } 47 | } 48 | 49 | beforeRender() 50 | { 51 | if(!this.active) 52 | { 53 | return 54 | } 55 | 56 | // Setup 57 | this.queryCreated = false 58 | let queryResultAvailable = false 59 | 60 | // Test if query result available 61 | if(this.render.query) 62 | { 63 | queryResultAvailable = this.render.context.getQueryParameter(this.render.query, this.render.context.QUERY_RESULT_AVAILABLE) 64 | const disjoint = this.render.context.getParameter(this.render.extension.GPU_DISJOINT_EXT) 65 | 66 | if(queryResultAvailable && !disjoint) 67 | { 68 | const elapsedNanos = this.render.context.getQueryParameter(this.render.query, this.render.context.QUERY_RESULT) 69 | const panelValue = Math.min(elapsedNanos / 1000 / 1000, this.max) 70 | 71 | if(panelValue === this.max && this.ignoreMaxed) 72 | { 73 | 74 | } 75 | else 76 | { 77 | this.render.panel.update(panelValue, this.max) 78 | } 79 | } 80 | } 81 | 82 | // If query result available or no query yet 83 | if(queryResultAvailable || !this.render.query) 84 | { 85 | // Create new query 86 | this.queryCreated = true 87 | this.render.query = this.render.context.createQuery() 88 | this.render.context.beginQuery(this.render.extension.TIME_ELAPSED_EXT, this.render.query) 89 | } 90 | 91 | } 92 | 93 | afterRender() 94 | { 95 | if(!this.active) 96 | { 97 | return 98 | } 99 | 100 | // End the query (result will be available "later") 101 | if(this.queryCreated) 102 | { 103 | this.render.context.endQuery(this.render.extension.TIME_ELAPSED_EXT) 104 | } 105 | } 106 | 107 | update() 108 | { 109 | if(!this.active) 110 | { 111 | return 112 | } 113 | 114 | this.instance.update() 115 | } 116 | 117 | destroy() 118 | { 119 | this.deactivate() 120 | } 121 | } -------------------------------------------------------------------------------- /src/Experience/Utils/Time.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './EventEmitter.js' 2 | 3 | export default class Time extends EventEmitter 4 | { 5 | /** 6 | * Constructor 7 | */ 8 | constructor() 9 | { 10 | super() 11 | 12 | this.start = Date.now() 13 | this.current = this.start 14 | this.elapsed = 0 15 | this.delta = 16 16 | this.playing = true 17 | 18 | this.tick = this.tick.bind(this) 19 | this.tick() 20 | } 21 | 22 | play() 23 | { 24 | this.playing = true 25 | } 26 | 27 | pause() 28 | { 29 | this.playing = false 30 | } 31 | 32 | /** 33 | * Tick 34 | */ 35 | tick() 36 | { 37 | this.ticker = window.requestAnimationFrame(this.tick) 38 | 39 | const current = Date.now() 40 | 41 | this.delta = current - this.current 42 | this.elapsed += this.playing ? this.delta : 0 43 | this.current = current 44 | 45 | if(this.delta > 60) 46 | { 47 | this.delta = 60 48 | } 49 | 50 | if(this.playing) 51 | { 52 | this.trigger('tick') 53 | } 54 | } 55 | 56 | /** 57 | * Stop 58 | */ 59 | stop() 60 | { 61 | window.cancelAnimationFrame(this.ticker) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Experience/World.js: -------------------------------------------------------------------------------- 1 | import Experience from "./Experience.js"; 2 | import Baked from "./Baked.js"; 3 | 4 | export default class World { 5 | constructor(_options) { 6 | this.experience = new Experience(); 7 | this.resources = this.experience.resources; 8 | 9 | this.resources.on("groupEnd", (_group) => { 10 | if (_group.name === "base") { 11 | this.setBaked(); 12 | } 13 | }); 14 | } 15 | 16 | setBaked() { 17 | this.baked = new Baked(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Experience/assets.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'base', 4 | data: {}, 5 | items: 6 | [ 7 | { name: 'bakedDayTexture', source: './assets/bakedDay.jpg', type: 'texture' }, 8 | { name: 'bakedNightTexture', source: './assets/bakedNight.jpg', type: 'texture' }, 9 | { name: 'lightMapTexture', source: './assets/lightMap.jpg', type: 'texture' }, 10 | { name: 'roomModel', source: './assets/city.glb' }, 11 | ] 12 | } 13 | ] -------------------------------------------------------------------------------- /src/Experience/shaders/baked/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uBakedDayTexture; 2 | uniform sampler2D uBakedNightTexture; 3 | uniform sampler2D uLightMapTexture; 4 | 5 | uniform float uNightMix; 6 | 7 | uniform vec3 uLightWindowColor; 8 | uniform float uLightWindowStrength; 9 | 10 | uniform vec3 uLightLamOtherolor; 11 | uniform float uLightLampStrength; 12 | 13 | uniform vec3 uLightOtherColor; 14 | uniform float uLightOtherStrength; 15 | 16 | varying vec2 vUv; 17 | 18 | // #pragma glslify: blend = require(glsl-blend/add) 19 | // #pragma glslify: blend = require(glsl-blend/lighten) // 混合 20 | #pragma glslify: blend = require(glsl-blend/normal) 21 | // #pragma glslify: blend = require(glsl-blend/screen) 22 | 23 | void main() 24 | { 25 | vec3 bakedDayColor = texture2D(uBakedDayTexture, vUv).rgb; 26 | vec3 bakedNightColor = texture2D(uBakedNightTexture, vUv).rgb; 27 | vec3 bakedColor = mix(bakedDayColor, bakedNightColor, uNightMix); 28 | vec3 lightMaOtherolor = texture2D(uLightMapTexture, vUv).rgb; 29 | 30 | float lightLightStrength = lightMaOtherolor.b * uLightWindowStrength; 31 | bakedColor = blend(bakedColor, uLightWindowColor, lightLightStrength); 32 | 33 | float lightOtherStrength = lightMaOtherolor.g * uLightOtherStrength; 34 | bakedColor = blend(bakedColor, uLightOtherColor, lightOtherStrength); 35 | 36 | float lightLampStrength = lightMaOtherolor.r * uLightLampStrength; 37 | bakedColor = blend(bakedColor, uLightLamOtherolor, lightLampStrength); 38 | 39 | gl_FragColor = vec4(bakedColor, 1.0); 40 | } -------------------------------------------------------------------------------- /src/Experience/shaders/baked/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 | vec4 viewPosition = viewMatrix * modelPosition; 7 | vec4 projectionPosition = projectionMatrix * viewPosition; 8 | gl_Position = projectionPosition; 9 | 10 | vUv = uv; 11 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |