├── static ├── .gitkeep ├── model │ └── shanghai.FBX └── textures │ └── flag-french.jpg ├── img.gif ├── src ├── effect │ ├── index.js │ ├── wall.js │ ├── fly.js │ └── radar.js ├── utils │ ├── effect.js │ ├── index.js │ └── shader.js ├── style.css ├── index.html ├── script.js └── city.js ├── .gitignore ├── bundler ├── webpack.prod.js ├── webpack.dev.js └── webpack.common.js ├── readme.md └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonerao/Technology-City/HEAD/img.gif -------------------------------------------------------------------------------- /static/model/shanghai.FBX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonerao/Technology-City/HEAD/static/model/shanghai.FBX -------------------------------------------------------------------------------- /static/textures/flag-french.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonerao/Technology-City/HEAD/static/textures/flag-french.jpg -------------------------------------------------------------------------------- /src/effect/index.js: -------------------------------------------------------------------------------- 1 | import Radar from './radar'; 2 | import Wall from './wall'; 3 | import Fly from './fly'; 4 | export { 5 | Radar, 6 | Wall, 7 | Fly 8 | } -------------------------------------------------------------------------------- /src/utils/effect.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | export default { 3 | // 获取到包围的线条 4 | surroundLineGeometry(object) { 5 | return new THREE.EdgesGeometry(object.geometry); 6 | } 7 | } -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | * 2 | { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html, 8 | body 9 | { 10 | overflow: hidden; 11 | } 12 | 13 | .webgl 14 | { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | outline: none; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CITY 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /effects 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 公共方法 3 | */ 4 | // import * as THREE from 'three' 5 | 6 | export default { 7 | forMaterial(materials, callback) { 8 | if (!callback || !materials) return false; 9 | if (Array.isArray(materials)) { 10 | materials.forEach((mat) => { 11 | callback(mat); 12 | }); 13 | } else { 14 | callback(materials); 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Three.js 2 | ## 当前效果仅供学习 3 | 4 | ![效果图](https://raw.githubusercontent.com/stonerao/Technology-City/master/img.gif) 5 | 6 | ## Setup 7 | Download [Node.js](https://nodejs.org/en/download/). 8 | Run this followed commands: 9 | 10 | ``` bash 11 | # Install dependencies (only the first time) 12 | npm install 13 | 14 | # Run the local server at localhost:8080 15 | npm run dev 16 | 17 | # Build for production in the dist/ directory 18 | npm run build 19 | ``` 20 | 21 | 22 | ### 效果 23 | * effect/fly.js 飞线 24 | * effect/radar.js 雷达 25 | * effect/wall.js 光墙 -------------------------------------------------------------------------------- /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 | }, 8 | "dependencies": { 9 | "@babel/core": "^7.14.3", 10 | "@babel/preset-env": "^7.14.2", 11 | "babel-loader": "^8.2.2", 12 | "clean-webpack-plugin": "^3.0.0", 13 | "copy-webpack-plugin": "^9.0.0", 14 | "css-loader": "^5.2.6", 15 | "dat.gui": "^0.7.7", 16 | "file-loader": "^6.2.0", 17 | "html-loader": "^2.1.2", 18 | "html-webpack-plugin": "^5.3.1", 19 | "mini-css-extract-plugin": "^1.6.0", 20 | "portfinder-sync": "0.0.2", 21 | "raw-loader": "^4.0.2", 22 | "style-loader": "^2.0.0", 23 | "three": "^0.129.0", 24 | "webpack": "^5.38.0", 25 | "webpack-cli": "^4.7.0", 26 | "webpack-dev-server": "^3.11.2", 27 | "webpack-merge": "^5.7.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/effect/wall.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const vertexShader = ` 4 | uniform vec3 u_color; 5 | 6 | uniform float time; 7 | uniform float u_height; 8 | 9 | varying float v_opacity; 10 | 11 | void main() { 12 | 13 | vec3 vPosition = position * mod(time, 1.0); 14 | 15 | v_opacity = mix(1.0, 0.0, position.y / u_height); 16 | 17 | gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0); 18 | } 19 | `; 20 | const fragmentShader = ` 21 | uniform vec3 u_color; 22 | uniform float u_opacity; 23 | 24 | varying float v_opacity; 25 | 26 | void main() { 27 | gl_FragColor = vec4(u_color, v_opacity * u_opacity); 28 | } 29 | `; 30 | 31 | 32 | export default function (option = {}) { 33 | const { 34 | radius, 35 | height, 36 | opacity, 37 | color, 38 | speed, 39 | renderOrder 40 | } = option; 41 | const geometry = new THREE.CylinderGeometry(radius, radius, height, 32, 1, true); 42 | geometry.translate(0, height / 2, 0); 43 | const material = new THREE.ShaderMaterial({ 44 | uniforms: { 45 | u_height: { 46 | value: height 47 | }, 48 | u_speed: { 49 | value: speed || 1 50 | }, 51 | u_opacity: { 52 | value: opacity 53 | }, 54 | u_color: { 55 | value: new THREE.Color(color) 56 | }, 57 | time: { 58 | value: 0 59 | } 60 | }, 61 | transparent: true, 62 | depthWrite: false, 63 | depthTest: false, 64 | side: THREE.DoubleSide, 65 | vertexShader: vertexShader, 66 | fragmentShader: fragmentShader 67 | }); 68 | const mesh = new THREE.Mesh(geometry, material); 69 | mesh.renderOrder = renderOrder || 1; 70 | return mesh; 71 | } -------------------------------------------------------------------------------- /src/utils/shader.js: -------------------------------------------------------------------------------- 1 | const base = ` 2 | precision mediump float; 3 | 4 | float distanceTo(vec2 src, vec2 dst) { 5 | float dx = src.x - dst.x; 6 | float dy = src.y - dst.y; 7 | float dv = dx * dx + dy * dy; 8 | return sqrt(dv); 9 | } 10 | 11 | float lerp(float x, float y, float t) { 12 | return (1.0 - t) * x + t * y; 13 | } 14 | 15 | #define PI 3.14159265359 16 | #define PI2 6.28318530718 17 | 18 | ` 19 | const surroundLine = { 20 | // 顶点着色器 21 | vertexShader: ` 22 | #define PI 3.14159265359 23 | 24 | uniform mediump float uStartTime; 25 | uniform mediump float time; 26 | uniform mediump float uRange; 27 | uniform mediump float uSpeed; 28 | 29 | uniform vec3 uColor; 30 | uniform vec3 uActive; 31 | uniform vec3 uMin; 32 | uniform vec3 uMax; 33 | 34 | varying vec3 vColor; 35 | 36 | float lerp(float x, float y, float t) { 37 | return (1.0 - t) * x + t * y; 38 | } 39 | void main() { 40 | if (uStartTime >= 0.99) { 41 | float iTime = mod(time * uSpeed - uStartTime, 1.0); 42 | float rangeY = lerp(uMin.y, uMax.y, iTime); 43 | if (rangeY < position.y && rangeY > position.y - uRange) { 44 | float index = 1.0 - sin((position.y - rangeY) / uRange * PI); 45 | float r = lerp(uActive.r, uColor.r, index); 46 | float g = lerp(uActive.g, uColor.g, index); 47 | float b = lerp(uActive.b, uColor.b, index); 48 | 49 | vColor = vec3(r, g, b); 50 | } else { 51 | vColor = uColor; 52 | } 53 | } 54 | vec3 vPosition = vec3(position.x, position.y, position.z * uStartTime); 55 | gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0); 56 | } 57 | `, 58 | // 片元着色器 59 | fragmentShader: ` 60 | ${base} 61 | uniform float time; 62 | uniform float uOpacity; 63 | uniform float uStartTime; 64 | 65 | varying vec3 vColor; 66 | 67 | void main() { 68 | 69 | gl_FragColor = vec4(vColor, uOpacity * uStartTime); 70 | } 71 | ` 72 | } 73 | export default { 74 | base, 75 | surroundLine 76 | } -------------------------------------------------------------------------------- /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 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | import * as THREE from 'three' 3 | import { 4 | OrbitControls 5 | } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import * as dat from 'dat.gui' 7 | import CityClass from './city'; 8 | /** 9 | * Base 10 | */ 11 | // Debug 12 | const gui = new dat.GUI() 13 | 14 | // Canvas 15 | const canvas = document.querySelector('canvas.webgl') 16 | 17 | // Scene 18 | const scene = new THREE.Scene() 19 | 20 | /** 21 | * Textures 22 | */ 23 | const textureLoader = new THREE.TextureLoader() 24 | 25 | /** 26 | * Light 27 | */ 28 | const light = new THREE.AmbientLight(0xadadad); // soft white light 29 | scene.add(light); 30 | 31 | const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); 32 | directionalLight.position.set(100, 100, 0); 33 | scene.add(directionalLight); 34 | 35 | /** 36 | * Sizes 37 | */ 38 | const sizes = { 39 | width: window.innerWidth, 40 | height: window.innerHeight 41 | } 42 | 43 | window.addEventListener('resize', () => { 44 | // Update sizes 45 | sizes.width = window.innerWidth 46 | sizes.height = window.innerHeight 47 | 48 | // Update camera 49 | camera.aspect = sizes.width / sizes.height 50 | camera.updateProjectionMatrix() 51 | 52 | // Update renderer 53 | renderer.setSize(sizes.width, sizes.height); 54 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 55 | }) 56 | 57 | /** 58 | * Camera 59 | */ 60 | // Base camera 61 | const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 1, 10000) 62 | camera.position.set(1200, 700, 121) 63 | scene.add(camera) 64 | 65 | // Controls 66 | const controls = new OrbitControls(camera, canvas) 67 | controls.enableDamping = true 68 | 69 | /** 70 | * Renderer 71 | */ 72 | const renderer = new THREE.WebGLRenderer({ 73 | canvas: canvas, 74 | antialias: true, 75 | alpha: true 76 | }) 77 | renderer.setSize(sizes.width, sizes.height) 78 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 79 | renderer.setClearColor(new THREE.Color('#32373E'), 1); 80 | 81 | // City 82 | const city = new CityClass({}); 83 | scene.add(city.group); 84 | 85 | /** 86 | * Animate 87 | */ 88 | const clock = new THREE.Clock() 89 | 90 | const tick = () => { 91 | const dt = clock.getDelta(); 92 | 93 | city.animate(dt); 94 | 95 | // Update controls 96 | controls.update() 97 | 98 | // Render 99 | renderer.render(scene, camera) 100 | 101 | // Call tick again on the next frame 102 | window.requestAnimationFrame(tick) 103 | } 104 | 105 | tick() -------------------------------------------------------------------------------- /src/effect/fly.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | export default function (option) { 3 | const { 4 | source, 5 | target, 6 | height, 7 | size, 8 | color, 9 | range 10 | } = option; 11 | const positions = []; 12 | const attrPositions = []; 13 | const attrCindex = []; 14 | const attrCnumber = []; 15 | 16 | const _source = new THREE.Vector3(source.x, source.y, source.z); 17 | const _target = new THREE.Vector3(target.x, target.y, target.z); 18 | const _center = _target.clone().lerp(_source, 0.5); 19 | _center.y += height; 20 | 21 | const number = parseInt(_source.distanceTo(_center) + _target.distanceTo(_center)); 22 | 23 | const curve = new THREE.QuadraticBezierCurve3( 24 | _source, 25 | _center, 26 | _target 27 | ); 28 | 29 | const points = curve.getPoints(number); 30 | 31 | // 粒子位置计算 32 | 33 | points.forEach((elem, i) => { 34 | const index = i / (number - 1); 35 | positions.push({ 36 | x: elem.x, 37 | y: elem.y, 38 | z: elem.z 39 | }); 40 | attrCindex.push(index); 41 | attrCnumber.push(i); 42 | }) 43 | 44 | 45 | positions.forEach((p) => { 46 | attrPositions.push(p.x, p.y, p.z); 47 | }) 48 | 49 | const geometry = new THREE.BufferGeometry(); 50 | 51 | geometry.setAttribute('position', new THREE.Float32BufferAttribute(attrPositions, 3)); 52 | // 传递当前所在位置 53 | geometry.setAttribute('index', new THREE.Float32BufferAttribute(attrCindex, 1)); 54 | geometry.setAttribute('current', new THREE.Float32BufferAttribute(attrCnumber, 1)); 55 | 56 | const shader = new THREE.ShaderMaterial({ 57 | transparent: true, 58 | depthWrite: false, 59 | depthTest: false, 60 | blending: THREE.AdditiveBlending, 61 | uniforms: { 62 | uColor: { 63 | value: new THREE.Color(color) // 颜色 64 | }, 65 | uRange: { 66 | value: range || 100 // 显示当前范围的个数 67 | }, 68 | uSize: { 69 | value: size // 粒子大小 70 | }, 71 | uTotal: { 72 | value: number // 当前粒子的所有的总数 73 | }, 74 | time: { 75 | value: 0 // 76 | } 77 | }, 78 | vertexShader: ` 79 | attribute float index; 80 | attribute float current; 81 | uniform float time; 82 | uniform float uSize; 83 | uniform float uRange; // 展示区间 84 | uniform float uTotal; // 粒子总数 85 | uniform vec3 uColor; 86 | varying vec3 vColor; 87 | varying float vOpacity; 88 | void main() { 89 | // 需要当前显示的索引 90 | float size = uSize; 91 | float showNumber = uTotal * mod(time, 1.1); 92 | if (showNumber > current && showNumber < current + uRange) { 93 | float uIndex = ((current + uRange) - showNumber) / uRange; 94 | size *= uIndex; 95 | vOpacity = 1.0; 96 | } else { 97 | vOpacity = 0.0; 98 | } 99 | 100 | // 顶点着色器计算后的Position 101 | vColor = uColor; 102 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 103 | gl_Position = projectionMatrix * mvPosition; 104 | // 大小 105 | gl_PointSize = size * 300.0 / (-mvPosition.z); 106 | }`, 107 | fragmentShader: ` 108 | varying vec3 vColor; 109 | varying float vOpacity; 110 | void main() { 111 | gl_FragColor = vec4(vColor, vOpacity); 112 | }` 113 | }); 114 | 115 | const point = new THREE.Points(geometry, shader); 116 | 117 | return point; 118 | } -------------------------------------------------------------------------------- /src/effect/radar.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const frag_basic = ` 4 | precision mediump float; 5 | 6 | float atan2(float y, float x){ 7 | float t0, t1, t2, t3, t4; 8 | t3 = abs(x); 9 | t1 = abs(y); 10 | t0 = max(t3, t1); 11 | t1 = min(t3, t1); 12 | t3 = float(1) / t0; 13 | t3 = t1 * t3; 14 | t4 = t3 * t3; 15 | t0 = -float(0.013480470); 16 | t0 = t0 * t4 + float(0.057477314); 17 | t0 = t0 * t4 - float(0.121239071); 18 | t0 = t0 * t4 + float(0.195635925); 19 | t0 = t0 * t4 - float(0.332994597); 20 | t0 = t0 * t4 + float(0.999995630); 21 | t3 = t0 * t3; 22 | t3 = (abs(y) > abs(x)) ? float(1.570796327) - t3 : t3; 23 | t3 = (x < 0.0) ? float(3.141592654) - t3 : t3; 24 | t3 = (y < 0.0) ? -t3 : t3; 25 | return t3; 26 | } 27 | // 计算距离 28 | float distanceTo(vec2 src, vec2 dst) { 29 | float dx = src.x - dst.x; 30 | float dy = src.y - dst.y; 31 | float dv = dx * dx + dy * dy; 32 | return sqrt(dv); 33 | } 34 | 35 | #define PI 3.14159265359 36 | #define PI2 6.28318530718 37 | 38 | uniform vec3 u_color; 39 | uniform float time; 40 | uniform float u_opacity; 41 | uniform float u_radius; 42 | uniform float u_width; 43 | uniform float u_speed; 44 | 45 | varying vec2 v_position; 46 | 47 | ` 48 | const Shader = { 49 | vertexShader: ` 50 | varying vec2 v_position; 51 | 52 | void main() { 53 | v_position = vec2(position.x, position.y); 54 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 55 | }`, 56 | fragmentShader: ` 57 | ${frag_basic} 58 | void main() { 59 | float d_time = u_speed * time; 60 | 61 | float angle = atan2(v_position.x, v_position.y) + PI; 62 | 63 | float angleT = mod(angle + d_time, PI2); 64 | 65 | float width = u_width; 66 | 67 | float d_opacity = 0.0; 68 | 69 | // 当前位置离中心位置 70 | float length = distanceTo(vec2(0.0, 0.0), v_position); 71 | 72 | float bw = 5.0; 73 | if (length < u_radius && length > u_radius - bw) { 74 | float o = (length - (u_radius - bw)) / bw; 75 | d_opacity = sin(o * PI); 76 | } 77 | 78 | if (length < u_radius - bw / 1.1) { 79 | d_opacity = 1.0 - angleT / PI * (PI / width); 80 | } 81 | 82 | if (length > u_radius) { d_opacity = 0.0; } 83 | 84 | gl_FragColor = vec4(u_color, d_opacity * u_opacity); 85 | }` 86 | } 87 | export default function (opts) { 88 | const { 89 | radius = 50, 90 | color = "#fff", 91 | speed = 1, 92 | opacity = 1, 93 | angle = Math.PI, 94 | position = { 95 | x: 0, 96 | y: 0, 97 | z: 0 98 | }, 99 | rotation = { 100 | x: -Math.PI / 2, 101 | y: 0, 102 | z: 0 103 | } 104 | } = opts; 105 | 106 | const width = radius * 2; 107 | 108 | const geometry = new THREE.PlaneBufferGeometry(width, width, 1, 1); 109 | 110 | const material = new THREE.ShaderMaterial({ 111 | uniforms: { 112 | u_radius: { 113 | value: radius 114 | }, 115 | u_speed: { 116 | value: speed 117 | }, 118 | u_opacity: { 119 | value: opacity 120 | }, 121 | u_width: { 122 | value: angle 123 | }, 124 | u_color: { 125 | value: new THREE.Color(color) 126 | }, 127 | time: { 128 | value: 0 129 | } 130 | }, 131 | transparent: true, 132 | depthWrite: false, 133 | side: THREE.DoubleSide, 134 | vertexShader: Shader.vertexShader, 135 | fragmentShader: Shader.fragmentShader 136 | }) 137 | 138 | const mesh = new THREE.Mesh(geometry, material); 139 | 140 | mesh.rotation.set(rotation.x, rotation.y, rotation.z); 141 | mesh.position.copy(position); 142 | 143 | return mesh 144 | } -------------------------------------------------------------------------------- /src/city.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { 3 | FBXLoader 4 | } from 'three/examples/jsm/loaders/FBXLoader.js' 5 | import Effects from './utils/effect' 6 | import Shader from './utils/shader' 7 | import Utils from './utils/index' 8 | import { 9 | Radar, 10 | Wall, 11 | Fly 12 | } from './effect/index' 13 | 14 | const radarData = [{ 15 | position: { 16 | x: 666, 17 | y: 22, 18 | z: 0 19 | }, 20 | radius: 150, 21 | color: '#ff0000', 22 | opacity: 0.5, 23 | speed: 2 24 | }, { 25 | position: { 26 | x: -666, 27 | y: 25, 28 | z: 202 29 | }, 30 | radius: 320, 31 | color: '#efad35', 32 | opacity: 0.6, 33 | speed: 1 34 | }]; 35 | const wallData = [{ 36 | position: { 37 | x: -150, 38 | y: 15, 39 | z: 100 40 | }, 41 | speed: 0.5, 42 | color: '#efad35', 43 | opacity: 0.6, 44 | radius: 420, 45 | height: 120, 46 | renderOrder: 5 47 | }] 48 | const flyData = [{ 49 | source: { 50 | x: -150, 51 | y: 15, 52 | z: 100 53 | }, 54 | target: { 55 | x: -666, 56 | y: 25, 57 | z: 202 58 | }, 59 | range: 120, 60 | height: 100, 61 | color: '#efad35', 62 | speed: 1, 63 | size: 30 64 | }, { 65 | source: { 66 | x: -150, 67 | y: 15, 68 | z: 100 69 | }, 70 | target: { 71 | x: 666, 72 | y: 22, 73 | z: 0 74 | }, 75 | height: 300, 76 | range: 150, 77 | color: '#ff0000', 78 | speed: 1, 79 | size: 40 80 | }] 81 | 82 | class City { 83 | constructor() { 84 | this.fbxLoader = new FBXLoader(); 85 | this.group = new THREE.Group(); 86 | 87 | this.effectGroup = new THREE.Group(); 88 | 89 | this.group.add(this.effectGroup); 90 | 91 | this.surroundLineMaterial = null; 92 | this.time = { 93 | value: 0 94 | }; 95 | this.StartTime = { 96 | value: 0 97 | }; 98 | this.isStart = false; // 是否启动 99 | 100 | // 需要做城市效果的mesh 101 | const cityArray = ['CITY_UNTRIANGULATED']; 102 | 103 | const floorArray = ['LANDMASS']; 104 | 105 | this.loadFbx('/model/shanghai.FBX').then((scene) => { 106 | this.group.add(scene); 107 | // 遍历整个场景找到对应的对象 108 | scene.traverse((child) => { 109 | // 城市效果 110 | if (cityArray.includes(child.name)) { 111 | // 建筑 112 | this.setCityMaterial(child); 113 | // 添加包围线条效 114 | this.surroundLine(child); 115 | } 116 | if (floorArray.includes(child.name)) { 117 | this.setFloor(child); 118 | } 119 | }) 120 | }); 121 | 122 | this.init(); 123 | } 124 | 125 | /** 126 | * Loader Model 127 | */ 128 | loadFbx(url) { 129 | return new Promise((resolve, reject) => { 130 | try { 131 | this.fbxLoader.load(url, (obj) => { 132 | resolve(obj); 133 | }); 134 | } catch (e) { 135 | reject(e); 136 | } 137 | }); 138 | } 139 | 140 | init() { 141 | setTimeout(() => { 142 | this.isStart = true; 143 | // 加载扫描效果 144 | radarData.forEach((data) => { 145 | const mesh = Radar(data); 146 | mesh.material.uniforms.time = this.time; 147 | this.effectGroup.add(mesh); 148 | }); 149 | // 光墙 150 | wallData.forEach((data) => { 151 | const mesh = Wall(data); 152 | mesh.material.uniforms.time = this.time; 153 | this.effectGroup.add(mesh); 154 | }); 155 | // 光墙 156 | flyData.forEach((data) => { 157 | const mesh = Fly(data); 158 | mesh.material.uniforms.time = this.time; 159 | mesh.renderOrder = 10; 160 | this.effectGroup.add(mesh); 161 | }); 162 | }, 1000); 163 | } 164 | 165 | // 设置地板 166 | setFloor(object) { 167 | Utils.forMaterial(object.material, (material) => { 168 | material.color.setStyle("#040912"); 169 | }) 170 | } 171 | // Mesh 172 | /** 173 | * 174 | */ 175 | setCityMaterial(object) { 176 | // 确定oject的geometry的box size 177 | object.geometry.computeBoundingBox(); 178 | object.geometry.computeBoundingSphere(); 179 | 180 | const { 181 | geometry 182 | } = object; 183 | 184 | // 获取geometry的长宽高 中心点 185 | const { 186 | center, 187 | radius 188 | } = geometry.boundingSphere; 189 | 190 | const { 191 | max, 192 | min 193 | } = geometry.boundingBox; 194 | 195 | const size = new THREE.Vector3( 196 | max.x - min.x, 197 | max.y - min.y, 198 | max.z - min.z 199 | ); 200 | 201 | Utils.forMaterial(object.material, (material) => { 202 | // material.opacity = 0.6; 203 | material.transparent = true; 204 | material.color.setStyle("#1B3045"); 205 | 206 | material.onBeforeCompile = (shader) => { 207 | shader.uniforms.time = this.time; 208 | shader.uniforms.uStartTime = this.StartTime; 209 | 210 | // 中心点 211 | shader.uniforms.uCenter = { 212 | value: center 213 | } 214 | 215 | // geometry大小 216 | shader.uniforms.uSize = { 217 | value: size 218 | } 219 | 220 | shader.uniforms.uMax = { 221 | value: max 222 | } 223 | 224 | shader.uniforms.uMin = { 225 | value: min 226 | } 227 | shader.uniforms.uTopColor = { 228 | value: new THREE.Color('#FFFFDC') 229 | } 230 | 231 | // 效果开关 232 | shader.uniforms.uSwitch = { 233 | value: new THREE.Vector3( 234 | 0, // 扩散 235 | 0, // 左右横扫 236 | 0 // 向上扫描 237 | ) 238 | }; 239 | // 扩散 240 | shader.uniforms.uDiffusion = { 241 | value: new THREE.Vector3( 242 | 1, // 0 1开关 243 | 120, // 范围 244 | 600 // 速度 245 | ) 246 | }; 247 | // 扩散中心点 248 | shader.uniforms.uDiffusionCenter = { 249 | value: new THREE.Vector3( 250 | 0, 0, 0 251 | ) 252 | }; 253 | 254 | // 扩散中心点 255 | shader.uniforms.uFlow = { 256 | value: new THREE.Vector3( 257 | 1, // 0 1开关 258 | 10, // 范围 259 | 20 // 速度 260 | ) 261 | }; 262 | 263 | // 效果颜色 264 | shader.uniforms.uColor = { 265 | value: new THREE.Color("#5588aa") 266 | } 267 | // 效果颜色 268 | shader.uniforms.uFlowColor = { 269 | value: new THREE.Color("#5588AA") 270 | } 271 | 272 | // 效果透明度 273 | shader.uniforms.uOpacity = { 274 | value: 1 275 | } 276 | 277 | // 效果透明度 278 | shader.uniforms.uRadius = { 279 | value: radius 280 | } 281 | shader.uniforms.uModRange = { value: 10 } // 范围 282 | shader.uniforms.uModWidth = { value: 1.5 } // 范围 283 | 284 | /** 285 | * 对片元着色器进行修改 286 | */ 287 | const fragment = ` 288 | float distanceTo(vec2 src, vec2 dst) { 289 | float dx = src.x - dst.x; 290 | float dy = src.y - dst.y; 291 | float dv = dx * dx + dy * dy; 292 | return sqrt(dv); 293 | } 294 | 295 | float lerp(float x, float y, float t) { 296 | return (1.0 - t) * x + t * y; 297 | } 298 | 299 | vec3 getGradientColor(vec3 color1, vec3 color2, float index) { 300 | float r = lerp(color1.r, color2.r, index); 301 | float g = lerp(color1.g, color2.g, index); 302 | float b = lerp(color1.b, color2.b, index); 303 | return vec3(r, g, b); 304 | } 305 | 306 | varying vec4 vPositionMatrix; 307 | varying vec3 vPosition; 308 | 309 | uniform float time; 310 | // 扩散参数 311 | uniform float uRadius; 312 | uniform float uOpacity; 313 | uniform float uModRange; 314 | uniform float uModWidth; 315 | // 初始动画参数 316 | uniform float uStartTime; 317 | 318 | uniform vec3 uMin; 319 | uniform vec3 uMax; 320 | uniform vec3 uSize; 321 | uniform vec3 uFlow; 322 | uniform vec3 uColor; 323 | uniform vec3 uCenter; 324 | uniform vec3 uSwitch; 325 | uniform vec3 uTopColor; 326 | uniform vec3 uFlowColor; 327 | uniform vec3 uDiffusion; 328 | uniform vec3 uDiffusionCenter; 329 | 330 | void main() { 331 | `; 332 | const fragmentColor = ` 333 | vec3 distColor = outgoingLight; 334 | float dstOpacity = diffuseColor.a; 335 | 336 | float indexMix = vPosition.z / (uSize.z * 0.6); 337 | distColor = mix(distColor, uTopColor, indexMix); 338 | 339 | // 开启扩散波 340 | vec2 position2D = vec2(vPosition.x, vPosition.y); 341 | float mx = mod(vPosition.x, uModRange); 342 | float my = mod(vPosition.y, uModRange); 343 | float mz = mod(vPosition.z, uModRange); 344 | 345 | if (uDiffusion.x > 0.5) { 346 | // 扩散速度 347 | float dTime = mod(time * uDiffusion.z, uRadius * 2.0); 348 | // 当前的离中心点距离 349 | float uLen = distanceTo(position2D, vec2(uCenter.x, uCenter.z)); 350 | 351 | // 扩散范围 352 | if (uLen < dTime && uLen > dTime - uDiffusion.y) { 353 | // 颜色渐变 354 | float dIndex = sin((dTime - uLen) / uDiffusion.y * PI); 355 | distColor = mix(uColor, distColor, 1.0 - dIndex); 356 | } 357 | 358 | // 扫描中间格子 359 | if (uLen < dTime) { 360 | if (mx < uModWidth || my < uModWidth || mz < uModWidth ) { 361 | distColor = vec3(0.7); 362 | } 363 | } 364 | } 365 | 366 | // 流动效果 367 | if (uFlow.x > 0.5) { 368 | // 扩散速度 369 | float dTime = mod(time * uFlow.z, uSize.z); 370 | // 流动范围 371 | float topY = vPosition.z + uFlow.y; 372 | if (dTime > vPosition.z && dTime < topY) { 373 | // 颜色渐变 374 | float dIndex = sin((topY - dTime) / uFlow.y * PI); 375 | 376 | distColor = mix(distColor, uFlowColor, dIndex); 377 | } 378 | } 379 | 380 | 381 | gl_FragColor = vec4(distColor, dstOpacity * uStartTime); 382 | `; 383 | shader.fragmentShader = shader.fragmentShader.replace("void main() {", fragment) 384 | shader.fragmentShader = shader.fragmentShader.replace("gl_FragColor = vec4( outgoingLight, diffuseColor.a );", fragmentColor); 385 | 386 | 387 | 388 | /** 389 | * 对顶点着色器进行修改 390 | */ 391 | const vertex = ` 392 | varying vec4 vPositionMatrix; 393 | varying vec3 vPosition; 394 | uniform float uStartTime; 395 | void main() { 396 | vPositionMatrix = projectionMatrix * vec4(position, 1.0); 397 | vPosition = position; 398 | ` 399 | const vertexPosition = ` 400 | vec3 transformed = vec3(position.x, position.y, position.z * uStartTime); 401 | ` 402 | 403 | shader.vertexShader = shader.vertexShader.replace("void main() {", vertex); 404 | shader.vertexShader = shader.vertexShader.replace("#include ", vertexPosition); 405 | } 406 | }) 407 | } 408 | 409 | // Line 410 | /** 411 | * 获取包围线条效果 412 | */ 413 | surroundLine(object) { 414 | // 获取线条geometry 415 | const geometry = Effects.surroundLineGeometry(object); 416 | // 获取物体的世界坐标 旋转等 417 | const worldPosition = new THREE.Vector3(); 418 | object.getWorldPosition(worldPosition); 419 | 420 | // 传递给shader重要参数 421 | const { 422 | max, 423 | min 424 | } = object.geometry.boundingBox; 425 | 426 | const size = new THREE.Vector3( 427 | max.x - min.x, 428 | max.y - min.y, 429 | max.z - min.z 430 | ); 431 | 432 | // this.effectGroup.add(); 433 | const material = this.createSurroundLineMaterial({ 434 | max, 435 | min, 436 | size 437 | }); 438 | 439 | const line = new THREE.LineSegments(geometry, material); 440 | 441 | line.name = 'surroundLine'; 442 | 443 | line.scale.copy(object.scale); 444 | line.rotation.copy(object.rotation); 445 | line.position.copy(worldPosition); 446 | 447 | this.effectGroup.add(line); 448 | } 449 | 450 | /** 451 | * 创建包围线条材质 452 | */ 453 | createSurroundLineMaterial({ 454 | max, 455 | min, 456 | size 457 | }) { 458 | if (this.surroundLineMaterial) return surroundLineMaterial; 459 | 460 | this.surroundLineMaterial = new THREE.ShaderMaterial({ 461 | transparent: true, 462 | uniforms: { 463 | uColor: { 464 | value: new THREE.Color("#4C8BF5") 465 | }, 466 | uActive: { 467 | value: new THREE.Color("#fff") 468 | }, 469 | time: this.time, 470 | uOpacity: { 471 | value: 0.6 472 | }, 473 | uMax: { 474 | value: max, 475 | }, 476 | uMin: { 477 | value: min, 478 | }, 479 | uRange: { 480 | value: 200 481 | }, 482 | uSpeed: { 483 | value: 0.2 484 | }, 485 | uStartTime: this.StartTime 486 | }, 487 | vertexShader: Shader.surroundLine.vertexShader, 488 | fragmentShader: Shader.surroundLine.fragmentShader 489 | }); 490 | 491 | return this.surroundLineMaterial; 492 | } 493 | 494 | animate = (dt) => { 495 | if (dt > 1) return false; 496 | this.time.value += dt; 497 | 498 | // 启动 499 | if (this.isStart) { 500 | this.StartTime.value += dt * 0.5; 501 | if (this.StartTime.value >= 1) { 502 | this.StartTime.value = 1; 503 | this.isStart = false; 504 | } 505 | } 506 | } 507 | } 508 | 509 | export default City; --------------------------------------------------------------------------------