├── 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 | 
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;
--------------------------------------------------------------------------------