前言
分形是一种具有自相似性的几何图形或数学对象。它的特点是无论在任何放大或缩小的尺度下,都能够看到与整体相似的图形。分形的形状可以非常复杂,常常具有分支、重复的图案,以及细节层次丰富的结构。
分形在自然界中广泛存在,如云朵、树枝、山脉、海岸线等,它们都展现了分形的特性。分形也在科学和艺术中得到广泛应用。在科学中,分形可以用来描述复杂的自然现象,如心电图、地理地貌、经济市场的波动等。在艺术中,分形可以用来生成艺术作品,如绘画、音乐、电影等,通过分形的自相似性和美学特性,创造出独特的视觉和听觉体验。
分形理论的发展起源于20世纪70年代,由数学家贝诺瓦·曼德布罗特和两个波兰数学家兹比格涅夫·尼科拉斯·普鲁斯金斯基及尼科拉斯·尼可夫发现和研究。分形理论为我们提供了一种新的视角来理解自然界和艺术中的复杂性,并且对人类的认知和创造力产生了深远的影响。
科赫雪花(也叫科赫雪花曲线)是一种分形图形,由瑞典数学家科尔·科赫(Helge von Koch)于1904年提出的。它是从一个等边三角形开始构建的,然后将每条边分成三等分,并在其中一等分边上构建一个新的等边三角形。不断重复这个过程,每次都在已经构建好的图形的每条边上重复。最终,会得到一个形状复杂且具有自相似性的图形。
科赫雪花具有许多有趣的属性。首先,它有无限的边界长度,也就是说,无论你如何继续细分边界,都无法得到一个封闭的图形。其次,科赫雪花的面积虽然有限,但无法通过传统的几何方法来计算。最后,科赫雪花是自相似的,即它的任何一部分都与整体具有相似的形状。
科赫雪花是分形几何学的一个经典例子,它展示了分形的一些基本概念和性质。
分形是一种具有自相似性的图形,无论在任何尺度上观察,都能看到相似的形状。这种自相似性是分形图形与传统几何图形的重要区别之一,使得分形图形在自然界和科学中的许多领域都有广泛的应用。
用three.js实现科赫雪花
<!DOCTYPE html>
<html class="fullscreen" lang="zh-CN">
<head>
<title>snow flake</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="">
<style>
.fullscreen {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: hidden;
}
html, body {
overflow: hidden;
font-family: '微软雅黑', sans-serif;
color: #2f2f2f;
}
/* Page Loader */
.page-loader {
width: calc(100vw - 15px);
height: calc(100vh - 15px);
background-color: #f5e6d3;
position: absolute;
z-index: 100;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.loader-container {
width: 200px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.base {
position: absolute;
width: 90px;
height: 51px;
}
.base-two {
transform: rotate(60deg)
}
.base-three {
transform: rotate(-60deg)
}
.loader-hex-edge {
position: absolute;
width: 4px;
height: 0%;
background: #00738B;
z-index: 101;
}
.h1 {
left: 0;
animation: AniOne 7.2s ease infinite;
}
.h2 {
right: 0;
animation: AniTwo 7.2s ease .6s infinite;
}
.h3 {
right: 0;
animation: AniTwo 7.2s ease 1.2s infinite;
}
.h4 {
right: 0;
animation: AniTwo 7.2s ease 1.8s infinite;
}
.h5 {
left: 0;
animation: AniOne 7.2s ease 2.4s infinite;
}
.h6 {
left: 0;
animation: AniOne 7.2s ease 3s infinite;
}
@keyframes AniOne {
0% {
bottom: 0;
height: 0;
}
6.944444444% {
bottom: 0;
height: 100%;
}
50% {
top: 0;
height: 100%;
}
59.944444433% {
top: 0;
height: 0;
}
}
@keyframes AniTwo {
0% {
top: 0;
height: 0;
}
6.944444444% {
top: 0;
height: 100%;
}
50% {
bottom: 0;
height: 100%;
}
59.944444433% {
bottom: 0;
height: 0;
}
}
/* Canvas */
.container {
width: calc(100vw - 1px);
height: calc(100vh - 1px);
position: absolute;
z-index: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.canvas {
border-radius: 1px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/",
"lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js",
"@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js"
}
}
</script>
</head>
<body class="fullscreen">
<div class="page-loader">
<div class="loader-container">
<div class="base">
<span class="loader-hex-edge h6"></span>
<span class="loader-hex-edge h3"></span>
</div>
<div class="base base-two">
<span class="loader-hex-edge h1"></span>
<span class="loader-hex-edge h4"></span>
</div>
<div class="base base-three">
<span class="loader-hex-edge h5"></span>
<span class="loader-hex-edge h2"></span>
</div>
</div>
</div>
<div class="fullscreen container">
<canvas class="fullscreen canvas"></canvas>
</div>
<script type="module">
import * as THREE from 'three';
import * as TWEEN from '@tweenjs/tween.js';
import { GUI } from 'lil-gui';
const _changeEvent = { type: 'change' };
const _startEvent = { type: 'start' };
const _endEvent = { type: 'end' };
const _ray = new THREE.Ray();
const _plane = new THREE.Plane();
const TILT_LIMIT = Math.cos( 70 * THREE.MathUtils.DEG2RAD );
class OrbitControls extends THREE.EventDispatcher {
constructor( object, domElement ) {
super();
this.object = object;
this.domElement = domElement;
this.domElement.style.touchAction = 'none'; // disable touch scroll
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new THREE.Vector3();
// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect
this.cursor = new THREE.Vector3();
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0;
this.maxDistance = Infinity;
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0;
this.maxZoom = Infinity;
// Limit camera target within a spherical area around the cursor
this.minTargetRadius = 0;
this.maxTargetRadius = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
this.minAzimuthAngle = - Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.05;
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.panSpeed = 1.0;
this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
this.zoomToCursor = false;
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
// The four arrow keys
this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };
// Mouse buttons
this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
// Touch fingers
this.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN };
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
// the target DOM element for key events
this._domElementKeyEvents = null;
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.getDistance = function () {
return this.object.position.distanceTo( this.target );
};
this.listenToKeyEvents = function ( domElement ) {
domElement.addEventListener( 'keydown', onKeyDown );
this._domElementKeyEvents = domElement;
};
this.stopListenToKeyEvents = function () {
this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
this._domElementKeyEvents = null;
};
this.saveState = function () {
scope.target0.copy( scope.target );
scope.position0.copy( scope.object.position );
scope.zoom0 = scope.object.zoom;
};
this.reset = function () {
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent( _changeEvent );
scope.update();
state = STATE.NONE;
};
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function () {
const offset = new THREE.Vector3();
// so camera.up is the orbit axis
const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
const quatInverse = quat.clone().invert();
const lastPosition = new THREE.Vector3();
const lastQuaternion = new THREE.Quaternion();
const lastTargetPosition = new THREE.Vector3();
const twoPI = 2 * Math.PI;
return function update( deltaTime = null ) {
const position = scope.object.position;
offset.copy( position ).sub( scope.target );
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion( quat );
// angle from z-axis around y-axis
spherical.setFromVector3( offset );
if ( scope.autoRotate && state === STATE.NONE ) {
rotateLeft( getAutoRotationAngle( deltaTime ) );
}
if ( scope.enableDamping ) {
spherical.theta += sphericalDelta.theta * scope.dampingFactor;
spherical.phi += sphericalDelta.phi * scope.dampingFactor;
} else {
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
}
// restrict theta to be between desired limits
let min = scope.minAzimuthAngle;
let max = scope.maxAzimuthAngle;
if ( isFinite( min ) && isFinite( max ) ) {
if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
if ( min <= max ) {
spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
} else {
spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
Math.max( min, spherical.theta ) :
Math.min( max, spherical.theta );
}
}
// restrict phi to be between desired limits
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
spherical.makeSafe();
// move target to panned location
if ( scope.enableDamping === true ) {
scope.target.addScaledVector( panOffset, scope.dampingFactor );
} else {
scope.target.add( panOffset );
}
// Limit the target distance from the cursor to create a sphere around the center of interest
scope.target.sub( scope.cursor );
scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius );
scope.target.add( scope.cursor );
let zoomChanged = false;
// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
// we adjust zoom later in these cases
if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {
spherical.radius = clampDistance( spherical.radius );
} else {
const prevRadius = spherical.radius;
spherical.radius = clampDistance( spherical.radius * scale );
zoomChanged = prevRadius != spherical.radius;
}
offset.setFromSpherical( spherical );
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion( quatInverse );
position.copy( scope.target ).add( offset );
scope.object.lookAt( scope.target );
if ( scope.enableDamping === true ) {
sphericalDelta.theta *= ( 1 - scope.dampingFactor );
sphericalDelta.phi *= ( 1 - scope.dampingFactor );
panOffset.multiplyScalar( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
panOffset.set( 0, 0, 0 );
}
// adjust camera position
if ( scope.zoomToCursor && performCursorZoom ) {
let newRadius = null;
if ( scope.object.isPerspectiveCamera ) {
// move the camera down the pointer ray
// this method avoids floating point error
const prevRadius = offset.length();
newRadius = clampDistance( prevRadius * scale );
const radiusDelta = prevRadius - newRadius;
scope.object.position.addScaledVector( dollyDirection, radiusDelta );
scope.object.updateMatrixWorld();
zoomChanged = !! radiusDelta;
} else if ( scope.object.isOrthographicCamera ) {
// adjust the ortho camera position based on zoom changes
const mouseBefore = new THREE.Vector3( mouse.x, mouse.y, 0 );
mouseBefore.unproject( scope.object );
const prevZoom = scope.object.zoom;
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
scope.object.updateProjectionMatrix();
zoomChanged = prevZoom !== scope.object.zoom;
const mouseAfter = new THREE.Vector3( mouse.x, mouse.y, 0 );
mouseAfter.unproject( scope.object );
scope.object.position.sub( mouseAfter ).add( mouseBefore );
scope.object.updateMatrixWorld();
newRadius = offset.length();
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
scope.zoomToCursor = false;
}
// handle the placement of the target
if ( newRadius !== null ) {
if ( this.screenSpacePanning ) {
// position the orbit target in front of the new camera position
scope.target.set( 0, 0, - 1 )
.transformDirection( scope.object.matrix )
.multiplyScalar( newRadius )
.add( scope.object.position );
} else {
// get the ray and translation plane to compute target
_ray.origin.copy( scope.object.position );
_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );
// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
// extremely large values
if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {
object.lookAt( scope.target );
} else {
_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );
_ray.intersectPlane( _plane, scope.target );
}
}
}
} else if ( scope.object.isOrthographicCamera ) {
const prevZoom = scope.object.zoom;
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
if ( prevZoom !== scope.object.zoom ) {
scope.object.updateProjectionMatrix();
zoomChanged = true;
}
}
scale = 1;
performCursorZoom = false;
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if ( zoomChanged ||
lastPosition.distanceToSquared( scope.object.position ) > EPS ||
8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||
lastTargetPosition.distanceToSquared( scope.target ) > EPS ) {
scope.dispatchEvent( _changeEvent );
lastPosition.copy( scope.object.position );
lastQuaternion.copy( scope.object.quaternion );
lastTargetPosition.copy( scope.target );
return true;
}
return false;
};
}();
this.dispose = function () {
scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
scope.domElement.removeEventListener( 'pointercancel', onPointerUp );
scope.domElement.removeEventListener( 'wheel', onMouseWheel );
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.removeEventListener( 'keydown', interceptControlDown, { capture: true } );
if ( scope._domElementKeyEvents !== null ) {
scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
scope._domElementKeyEvents = null;
}
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
};
//
// internals
//
const scope = this;
const STATE = {
NONE: - 1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6
};
let state = STATE.NONE;
const EPS = 0.000001;
// current position in spherical coordinates
const spherical = new THREE.Spherical();
const sphericalDelta = new THREE.Spherical();
let scale = 1;
const panOffset = new THREE.Vector3();
const rotateStart = new THREE.Vector2();
const rotateEnd = new THREE.Vector2();
const rotateDelta = new THREE.Vector2();
const panStart = new THREE.Vector2();
const panEnd = new THREE.Vector2();
const panDelta = new THREE.Vector2();
const dollyStart = new THREE.Vector2();
const dollyEnd = new THREE.Vector2();
const dollyDelta = new THREE.Vector2();
const dollyDirection = new THREE.Vector3();
const mouse = new THREE.Vector2();
let performCursorZoom = false;
const pointers = [];
const pointerPositions = {};
let controlActive = false;
function getAutoRotationAngle( deltaTime ) {
if ( deltaTime !== null ) {
return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime;
} else {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
}
function getZoomScale( delta ) {
const normalizedDelta = Math.abs( delta * 0.01 );
return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta );
}
function rotateLeft( angle ) {
sphericalDelta.theta -= angle;
}
function rotateUp( angle ) {
sphericalDelta.phi -= angle;
}
const panLeft = function () {
const v = new THREE.Vector3();
return function panLeft( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
v.multiplyScalar( - distance );
panOffset.add( v );
};
}();
const panUp = function () {
const v = new THREE.Vector3();
return function panUp( distance, objectMatrix ) {
if ( scope.screenSpacePanning === true ) {
v.setFromMatrixColumn( objectMatrix, 1 );
} else {
v.setFromMatrixColumn( objectMatrix, 0 );
v.crossVectors( scope.object.up, v );
}
v.multiplyScalar( distance );
panOffset.add( v );
};
}();
// deltaX and deltaY are in pixels; right and down are positive
const pan = function () {
const offset = new THREE.Vector3();
return function pan( deltaX, deltaY ) {
const element = scope.domElement;
if ( scope.object.isPerspectiveCamera ) {
// perspective
const position = scope.object.position;
offset.copy( position ).sub( scope.target );
let targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we use only clientHeight here so aspect ratio does not distort speed
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
} else if ( scope.object.isOrthographicCamera ) {
// orthographic
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
} else {
// camera neither orthographic nor perspective
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
scope.enablePan = false;
}
};
}();
function dollyOut( dollyScale ) {
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
scale /= dollyScale;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function dollyIn( dollyScale ) {
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
scale *= dollyScale;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function updateZoomParameters( x, y ) {
if ( ! scope.zoomToCursor ) {
return;
}
performCursorZoom = true;
const rect = scope.domElement.getBoundingClientRect();
const dx = x - rect.left;
const dy = y - rect.top;
const w = rect.width;
const h = rect.height;
mouse.x = ( dx / w ) * 2 - 1;
mouse.y = - ( dy / h ) * 2 + 1;
dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize();
}
function clampDistance( dist ) {
return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate( event ) {
rotateStart.set( event.clientX, event.clientY );
}
function handleMouseDownDolly( event ) {
updateZoomParameters( event.clientX, event.clientX );
dollyStart.set( event.clientX, event.clientY );
}
function handleMouseDownPan( event ) {
panStart.set( event.clientX, event.clientY );
}
function handleMouseMoveRotate( event ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
const element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleMouseMoveDolly( event ) {
dollyEnd.set( event.clientX, event.clientY );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyOut( getZoomScale( dollyDelta.y ) );
} else if ( dollyDelta.y < 0 ) {
dollyIn( getZoomScale( dollyDelta.y ) );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleMouseMovePan( event ) {
panEnd.set( event.clientX, event.clientY );
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleMouseWheel( event ) {
updateZoomParameters( event.clientX, event.clientY );
if ( event.deltaY < 0 ) {
dollyIn( getZoomScale( event.deltaY ) );
} else if ( event.deltaY > 0 ) {
dollyOut( getZoomScale( event.deltaY ) );
}
scope.update();
}
function handleKeyDown( event ) {
let needsUpdate = false;
switch ( event.code ) {
case scope.keys.UP:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( 0, scope.keyPanSpeed );
}
needsUpdate = true;
break;
case scope.keys.BOTTOM:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( 0, - scope.keyPanSpeed );
}
needsUpdate = true;
break;
case scope.keys.LEFT:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( scope.keyPanSpeed, 0 );
}
needsUpdate = true;
break;
case scope.keys.RIGHT:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( - scope.keyPanSpeed, 0 );
}
needsUpdate = true;
break;
}
if ( needsUpdate ) {
// prevent the browser from scrolling on cursor keys
event.preventDefault();
scope.update();
}
}
function handleTouchStartRotate( event ) {
if ( pointers.length === 1 ) {
rotateStart.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
rotateStart.set( x, y );
}
}
function handleTouchStartPan( event ) {
if ( pointers.length === 1 ) {
panStart.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
panStart.set( x, y );
}
}
function handleTouchStartDolly( event ) {
const position = getSecondPointerPosition( event );
const dx = event.pageX - position.x;
const dy = event.pageY - position.y;
const distance = Math.sqrt( dx * dx + dy * dy );
dollyStart.set( 0, distance );
}
function handleTouchStartDollyPan( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enablePan ) handleTouchStartPan( event );
}
function handleTouchStartDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enableRotate ) handleTouchStartRotate( event );
}
function handleTouchMoveRotate( event ) {
if ( pointers.length == 1 ) {
rotateEnd.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
rotateEnd.set( x, y );
}
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
const element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
}
function handleTouchMovePan( event ) {
if ( pointers.length === 1 ) {
panEnd.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
panEnd.set( x, y );
}
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
}
function handleTouchMoveDolly( event ) {
const position = getSecondPointerPosition( event );
const dx = event.pageX - position.x;
const dy = event.pageY - position.y;
const distance = Math.sqrt( dx * dx + dy * dy );
dollyEnd.set( 0, distance );
dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
dollyOut( dollyDelta.y );
dollyStart.copy( dollyEnd );
const centerX = ( event.pageX + position.x ) * 0.5;
const centerY = ( event.pageY + position.y ) * 0.5;
updateZoomParameters( centerX, centerY );
}
function handleTouchMoveDollyPan( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enablePan ) handleTouchMovePan( event );
}
function handleTouchMoveDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enableRotate ) handleTouchMoveRotate( event );
}
//
// event handlers - FSM: listen for events and reset state
//
function onPointerDown( event ) {
if ( scope.enabled === false ) return;
if ( pointers.length === 0 ) {
scope.domElement.setPointerCapture( event.pointerId );
scope.domElement.addEventListener( 'pointermove', onPointerMove );
scope.domElement.addEventListener( 'pointerup', onPointerUp );
}
//
if ( isTrackingPointer( event ) ) return;
//
addPointer( event );
if ( event.pointerType === 'touch' ) {
onTouchStart( event );
} else {
onMouseDown( event );
}
}
function onPointerMove( event ) {
if ( scope.enabled === false ) return;
if ( event.pointerType === 'touch' ) {
onTouchMove( event );
} else {
onMouseMove( event );
}
}
function onPointerUp( event ) {
removePointer( event );
switch ( pointers.length ) {
case 0:
scope.domElement.releasePointerCapture( event.pointerId );
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
scope.dispatchEvent( _endEvent );
state = STATE.NONE;
break;
case 1:
const pointerId = pointers[ 0 ];
const position = pointerPositions[ pointerId ];
// minimal placeholder event - allows state correction on pointer-up
onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
break;
}
}
function onMouseDown( event ) {
let mouseAction;
switch ( event.button ) {
case 0:
mouseAction = scope.mouseButtons.LEFT;
break;
case 1:
mouseAction = scope.mouseButtons.MIDDLE;
break;
case 2:
mouseAction = scope.mouseButtons.RIGHT;
break;
default:
mouseAction = - 1;
}
switch ( mouseAction ) {
case THREE.MOUSE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseDownDolly( event );
state = STATE.DOLLY;
break;
case THREE.MOUSE.ROTATE:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
} else {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
}
break;
case THREE.MOUSE.PAN:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
} else {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( _startEvent );
}
}
function onMouseMove( event ) {
switch ( state ) {
case STATE.ROTATE:
if ( scope.enableRotate === false ) return;
handleMouseMoveRotate( event );
break;
case STATE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseMoveDolly( event );
break;
case STATE.PAN:
if ( scope.enablePan === false ) return;
handleMouseMovePan( event );
break;
}
}
function onMouseWheel( event ) {
if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
event.preventDefault();
scope.dispatchEvent( _startEvent );
handleMouseWheel( customWheelEvent( event ) );
scope.dispatchEvent( _endEvent );
}
function customWheelEvent( event ) {
const mode = event.deltaMode;
// minimal wheel event altered to meet delta-zoom demand
const newEvent = {
clientX: event.clientX,
clientY: event.clientY,
deltaY: event.deltaY,
};
switch ( mode ) {
case 1: // LINE_MODE
newEvent.deltaY *= 16;
break;
case 2: // PAGE_MODE
newEvent.deltaY *= 100;
break;
}
// detect if event was triggered by pinching
if ( event.ctrlKey && ! controlActive ) {
newEvent.deltaY *= 10;
}
return newEvent;
}
function interceptControlDown( event ) {
if ( event.key === 'Control' ) {
controlActive = true;
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
}
}
function interceptControlUp( event ) {
if ( event.key === 'Control' ) {
controlActive = false;
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
}
}
function onKeyDown( event ) {
if ( scope.enabled === false || scope.enablePan === false ) return;
handleKeyDown( event );
}
function onTouchStart( event ) {
trackPointer( event );
switch ( pointers.length ) {
case 1:
switch ( scope.touches.ONE ) {
case THREE.TOUCH.ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchStartRotate( event );
state = STATE.TOUCH_ROTATE;
break;
case THREE.TOUCH.PAN:
if ( scope.enablePan === false ) return;
handleTouchStartPan( event );
state = STATE.TOUCH_PAN;
break;
default:
state = STATE.NONE;
}
break;
case 2:
switch ( scope.touches.TWO ) {
case THREE.TOUCH.DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchStartDollyPan( event );
state = STATE.TOUCH_DOLLY_PAN;
break;
case THREE.TOUCH.DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchStartDollyRotate( event );
state = STATE.TOUCH_DOLLY_ROTATE;
break;
default:
state = STATE.NONE;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( _startEvent );
}
}
function onTouchMove( event ) {
trackPointer( event );
switch ( state ) {
case STATE.TOUCH_ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchMoveRotate( event );
scope.update();
break;
case STATE.TOUCH_PAN:
if ( scope.enablePan === false ) return;
handleTouchMovePan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchMoveDollyPan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchMoveDollyRotate( event );
scope.update();
break;
default:
state = STATE.NONE;
}
}
function onContextMenu( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
}
function addPointer( event ) {
pointers.push( event.pointerId );
}
function removePointer( event ) {
delete pointerPositions[ event.pointerId ];
for ( let i = 0; i < pointers.length; i ++ ) {
if ( pointers[ i ] == event.pointerId ) {
pointers.splice( i, 1 );
return;
}
}
}
function isTrackingPointer( event ) {
for ( let i = 0; i < pointers.length; i ++ ) {
if ( pointers[ i ] == event.pointerId ) return true;
}
return false;
}
function trackPointer( event ) {
let position = pointerPositions[ event.pointerId ];
if ( position === undefined ) {
position = new THREE.Vector2();
pointerPositions[ event.pointerId ] = position;
}
position.set( event.pageX, event.pageY );
}
function getSecondPointerPosition( event ) {
const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ];
return pointerPositions[ pointerId ];
}
//
scope.domElement.addEventListener( 'contextmenu', onContextMenu );
scope.domElement.addEventListener( 'pointerdown', onPointerDown );
scope.domElement.addEventListener( 'pointercancel', onPointerUp );
scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } );
// force an update at start
this.update();
}
}
const container = document.querySelector('.container');
const canvas = document.querySelector('.canvas');
let
gpuTier,
sizes,
scene,
camera,
controls,
camX,
camY,
camZ,
renderer,
clock,
raycaster,
distance,
thirdPerson,
doubleSpeed,
mainLight,
lightHem,
currentPos,
currentLookAt,
lookAtPosZ,
mixer,
textures,
terrainTiles,
activeTile,
activeKeysPressed,
loadingDismissed;
const gui = new GUI({
width: 280,
title: 'Setting',
})
var params = new function() {
this.color = 0x123456; //颜色
this.length = 10; //几何体的大小
this.size = 0.01; //粒子大小
this.state = 'point'; //默认几何体
this.states = ['point', 'sphere', 'cube']; //几何体种类
this.visible = true; //是否显示几何体
};
let pointsGeometry = null;
let pointsMaterial = null;
let snowflakePoint = null;
let objGeometry = null;
gui.addColor(params, "color").onChange(e => { //点击颜色面板,e为返回的10进制颜色
pointsMaterial.color.set(e);
});
gui.add(params, "length", 0.1, 100).onChange(e => { //该滑块的值域是[1,100],e为返回的滑块值
snowflakePoint.scale.set( params.length, params.length, params.length );
snowflakePoint.geometry.verticesNeedUpdate = true;
})
gui.add(params, "size", 0.01, 10).onChange(e => { //同上
pointsMaterial.size = e
});
gui.add(params, "state").options(params.states).onChange( e => { //这是一个下拉列表,state是默认值,列表项通过options设置,params.states为列表数组,e返回所选的列表项。
scene.remove(scene.getObjectByName('points'));
if(e == 'sphere') {
objGeometry = new THREE.SphereGeometry(1, 30, 18);
} else if( e == 'cube') {
objGeometry = new THREE.BoxGeometry(1, 1, 1, 10, 10, 10);
}
if(e == 'point'){
snowflakePoint = new THREE.Points(pointsGeometry, pointsMaterial);
snowflakePoint.position.set( 0, 1, 0 );
//snowflake.rotation.set( 0, 1, 0 );
snowflakePoint.scale.set( params.length, params.length, params.length );
// 几何体绕着x轴旋转90度
snowflakePoint.rotateX(Math.PI / 2);
snowflakePoint.castShadow = true;
}else{
snowflakePoint = new THREE.Mesh(objGeometry, new THREE.MeshPhongMaterial({color: params.color,wireframe: true}));
}
snowflakePoint.name = 'points';
scene.add(snowflakePoint);
})
gui.add(params, 'visible').onChange(e => { //这是一个单选框,因为params.visible是一个布尔值,e返回所选布尔值
snowflakePoint.visible = e;
})
class PositionGUI {
constructor(obj, name) {
this.obj = obj
this.name = name
}
get modify() {
return this.obj[this.name]
}
set modify(v) {
this.obj[this.name] = v
}
}
const setScene = async () => {
sizes = {
width: container.offsetWidth,
height: container.offsetHeight
};
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5e6d3);
camX = 100,
camY = 100,
camZ = 100;
camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 10000);
camera.position.set(camX, camY, camZ);
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera.updateProjectionMatrix();
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputEncoding = THREE.sRGBEncoding;
//开启阴影效果
renderer.shadowMap.enabled = true;
renderer.shadowMapEnabled=true;
//container.appendChild( renderer.domElement );
clock = new THREE.Clock();
//创建并设置大小
let cubeGeometry = new THREE.BoxGeometry(4,4,4);
//设置颜色线框显示否
let cubeMaterial = new THREE.MeshLambertMaterial({
color:0xffffff,
transparent: true,
opacity: 0.5,
wireframe:false
});
let cube = new THREE.Mesh(cubeGeometry,cubeMaterial);
//设置cube的位置
cube.position.x=-10;
cube.position.y=2;
cube.position.z=0;
// 对象是否渲染到阴影贴图中,默认值为false
cube.castShadow = true;
//cube添加到场景中
scene.add(cube);
let sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
let sphereMaterial = new THREE.MeshLambertMaterial({
color: 0x7777ff,
transparent: true,
opacity: 0.8,
wireframe: false});
const gradientMaterial = new THREE.ShaderMaterial({
side: THREE.DoubleSide, //双面显示
uniforms: {
topColor: { value: new THREE.Color(0xffffff) },
bottomColor: { value: new THREE.Color(0x886611) },
},
vertexShader: `
varying vec3 vPosition;
void main() {
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vPosition;
uniform vec3 topColor;
uniform vec3 bottomColor;
void main() {
float percent = (vPosition.y + 0.2) / 2.0; // 计算当前像素点在立方体高度上的百分比
vec3 color = mix(bottomColor, topColor, percent); // 根据百分比进行颜色插值
// 设置当前像素点的颜色 透明度设置0.1,在0~1之间,半透明
gl_FragColor = vec4(color,0.1);
}
`,
});
let sphere = new THREE.Mesh(sphereGeometry, gradientMaterial);// sphereMaterial gradientMaterial
sphere.position.x = 10;
sphere.position.y = 2;
sphere.position.z = 0;
// 对象是否渲染到阴影贴图中,默认值为false
sphere.castShadow = true;
scene.add(sphere);
let planeGeometry = new THREE.PlaneGeometry(60, 20);
let planeMaterial = new THREE.MeshLambertMaterial({color: 0x6688aa});
let plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 几何体绕着x轴旋转-90度
plane.rotateX(-Math.PI/2);
// 设置平面网格为接受阴影的投影面
plane.receiveShadow = true;
scene.add(plane);
let spotLight = new THREE.SpotLight();
spotLight.position.set(10, 15, 0);
spotLight.castShadow = true;
spotLight.color = new THREE.Color(0x26E250) // 给聚光灯修改颜色
spotLight.visible = true //显示聚光灯
// 聚光灯的角度设置为Math.PI/3
spotLight.angle= Math.PI/3
// 聚光灯强度设置为2
spotLight.intensity = 1000
// 聚光灯距离设置为15
spotLight.distance = 15
spotLight.shadowCameraNear = 2;
spotLight.shadowCameraFar = 200;
spotLight.shadowCameraFov = 30;
spotLight.target = cube //聚光灯指向cube对象
scene.add(spotLight);
//绘制聚光灯光源辅助线
let SpotLightHelper = new THREE.SpotLightHelper(spotLight, 'white');
//scene.add(SpotLightHelper);
let directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(-10, 15, 0);
directionalLight.castShadow = true;
directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;
directionalLight.distance = 15;
directionalLight.intensity = 0.5;
directionalLight.shadowMapHeight = 1024;
directionalLight.shadowMapWidth = 1024;
directionalLight.target= cube; //注意target只能是一个Object3D()对象。
scene.add(directionalLight);
//绘制平行灯光源辅助线
let DirectionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 'white');
//scene.add(DirectionalLightHelper);
let intensity = 5;
if ( renderer !== null && renderer._useLegacyLights === false ) intensity = 900;
mainLight = new THREE.PointLight( 0xffffff, intensity, 28, 2 );
mainLight.position.set( -10, 8, 0 );
mainLight.castShadow = true;
scene.add(mainLight);
// THREE.PointLight(点光源)、THREE.SpotLight(聚光源)。
// THREE.DirectionalLight(平行光光源)定义的光源是能够产生阴影的。
lightHem = new THREE.HemisphereLight(0xffffaa, 0x080820, 1);
scene.add(lightHem);
// 添加科赫雪花到三维场景 迭代维度为6
scene.add(createSnowflake(6));
//cleanUp(snowflake);
setCam();
resize();
listenTo();
const folder = gui.addFolder('CameraPosition')
folder.add(new PositionGUI(camera.position, 'x'), 'modify', 0, 200).name('x')
folder.add(new PositionGUI(camera.position, 'y'), 'modify', 0, 200).name('y')
folder.add(new PositionGUI(camera.position, 'z'), 'modify', 0, 200).name('z')
gui.add(camera, 'fov', 0.1, 180).onChange(updateCamera)
gui.add(camera, 'near', 0.1, 200).onChange(updateCamera)
gui.add(camera, 'far', 0.1, 200).onChange(updateCamera)
let loaderBoxContainer = document.querySelector('.loader-container')
// 样式可以修改
loaderBoxContainer.style.opacity = 0
loaderBoxContainer.style.duration = 0.6
let loaderBox = document.querySelector('.page-loader')
loaderBox.style.opacity = 0
loaderBox.style.duration = 0.6
// 隐藏元素
loaderBox.style.display = 'none'
loadingDismissed = true;
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
const near = 60
const far = 100
scene.fog = new THREE.Fog(0xf5e6d3, near, far);
animate();
// 设置动画
const action=new TWEEN.Tween({
x:snowflakePoint.position.x,
y:snowflakePoint.position.y,
z:snowflakePoint.position.z}) // 初始值
.to({x:Math.random(),y:Math.random(),z:Math.random()},10000) // 目标值,毫秒数
// 在动画执行期,不断被调用。其中obj为"to"里面的内容
.onUpdate(function(obj){
snowflakePoint.position.x=obj.x;
snowflakePoint.position.y=obj.y;
snowflakePoint.position.z=obj.z;
}).start()
// 动画开始缓动-类比加速器
//action.easing(TWEEN.Easing.Sinusoidal.In);
// 动画结束时缓动-类比减速刹车
//action.easing(TWEEN.Easing.Sinusoidal.Out);
// 同时设置In和Out
action.easing(TWEEN.Easing.Sinusoidal.InOut);
//镜头移动动画
const cameraAct=new TWEEN.Tween(camera.position)
.to({x:-30,y:30,z:0},3000)
// 相机移动时,焦点始终为模型的位置
.onUpdate(function(){
camera.lookAt(snowflakePoint.position)
}).start()
//模型淡入动画
const meshActIn = new TWEEN.Tween({ opacity: 0.0 })
.to({ opacity: 1.0 }, 3000)
// 动画开始:开启材质的透明度
.onStart(function () {
pointsMaterial.transparent = true;
})
.onUpdate(function (obj) {
pointsMaterial.opacity = obj.opacity;
})
// 动画结束:关闭材质的透明度
.onComplete(function () {
pointsMaterial.transparent = false;
}).start();
//模型淡出动画
const meshActOut =new TWEEN.Tween({opacity:pointsMaterial.opacity})
.to({opacity:0.0}, 3000)
// 动画开始:允许透明opacity属性才能生效
.onStart(function(){
pointsMaterial.transparent = true;
})
.onUpdate(function(obj){
pointsMaterial.opacity = obj.opacity
}).start();
}
const setCam = () => {
currentPos = new THREE.Vector3();
currentLookAt = new THREE.Vector3();
lookAtPosZ = 100;
thirdPerson = true;
doubleSpeed = false;
}
// 辅助相机
function updateCamera() {
camera.updateProjectionMatrix()
}
const resize = () => {
sizes = {
width: container.offsetWidth,
height: container.offsetHeight
};
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
}
const keyDown = (event) => {
if(infoModalDisplayed) return;
if(!activeKeysPressed.includes(event.keyCode))
activeKeysPressed.push(event.keyCode);
}
const keyUp = (event) => {
if(event.keyCode === 32) toggleDoubleSpeed();
if(event.keyCode === 90) toggleBirdsEyeView();
const index = activeKeysPressed.indexOf(event.keyCode);
activeKeysPressed.splice(index, 1);
}
const listenTo = () => {
window.addEventListener('resize', resize.bind(this));
window.addEventListener('keydown', keyDown.bind(this));
window.addEventListener('keyup', keyUp.bind(this));
}
const cleanUp = (obj) => {
if(obj.geometry && obj.material) {
obj.geometry.dispose();
obj.material.dispose();
}
else {
obj.traverse(el => {
if(el.isMesh) {
el.geometry.dispose();
el.material.dispose();
}
});
}
scene.remove(obj);
renderer.renderLists.dispose();
}
let angle = 0.001;
const render = () => {
//let v1 = new THREE.Vector3(1,1,1)
//snowflakePoint.rotateOnAxis(v1,angle)
//snowflakePoint.rotateX(angle)
//snowflakePoint.rotateY(angle)
snowflakePoint.rotateZ(angle)
lightHem.rotateY(angle)
if(loadingDismissed) {
if(mixer) mixer.update(clock.getDelta());
}
controls.update();
// 更新动画
TWEEN.update()
renderer.render(scene, camera);
}
const animate = () => {
render();
requestAnimationFrame(animate);
}
// 定义科赫曲线的绘制函数
function kochCurve(start, end, iterations) {
let points = [];
// 计算线段的长度和方向
let length = end.clone().sub(start).length();
let angle = end.clone().sub(start).normalize();
// 计算新的点
let p1 = start.clone().add(angle.clone().multiplyScalar(length / 3));
let p3 = end.clone().sub(angle.clone().multiplyScalar(length / 3));
let p2 = p1.clone().add(angle.clone().applyAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 3)).multiplyScalar(length / 3);
// 递归绘制曲线
if (iterations > 0) {
points = points.concat(kochCurve(start, p1, iterations - 1));
points = points.concat(kochCurve(p1, p2, iterations - 1));
points = points.concat(kochCurve(p2, p3, iterations - 1));
points = points.concat(kochCurve(p3, end, iterations - 1));
} else {
points.push(start);
points.push(p1);
points.push(p2);
points.push(p3);
}
return points;
}
// 创建雪花的函数
function createSnowflake(iterations) {
// 创建雪花的基础几何体 THREE.Points、THREE.Line
pointsGeometry = new THREE.BufferGeometry();
//创建一个空的几何体对象
let snowflakePoints = []
// 创建六条雪花的线段
let start = new THREE.Vector3(-1, 0, 0);
let end = new THREE.Vector3(1, 0, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
start = new THREE.Vector3(-0.5, 0.87, 0);
end = new THREE.Vector3(0.5, -0.87, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
start = new THREE.Vector3(-0.5, -0.87, 0);
end = new THREE.Vector3(0.5, 0.87, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
start = new THREE.Vector3(1, 0, 0);
end = new THREE.Vector3(-1, 0, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
start = new THREE.Vector3(0.5, -0.87, 0);
end = new THREE.Vector3(-0.5, 0.87, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
start = new THREE.Vector3(0.5, 0.87, 0);
end = new THREE.Vector3(-0.5, -0.87, 0);
snowflakePoints.push(...kochCurve(start, end, iterations));
let len = snowflakePoints.length;
let snowflakeShape = [];
for (let i = 0; i < len ; i++) {
snowflakeShape.push(snowflakePoints[i].x);
snowflakeShape.push(snowflakePoints[i].y);
snowflakeShape.push(snowflakePoints[i].z);
}
// console.log(snowflakeShape);
// 创建雪花几何体
const extrudeSettings = { depth: 1, bevelEnabled: true };
// 创建一个简单的矩形. 在这里我们左上和右下顶点被复制了两次。
// 因为在两个三角面片里,这两个顶点都需要被用到。
const vertices = new Float32Array(snowflakeShape);
//类型化数组创建顶点数据
//vertices = new Float32Array([
// -50, 0, 0, //顶点1坐标
// 50, 0, 0, //顶点2坐标
// 0, 100, 0, //顶点3坐标
// 0, -10, 0, //顶点4坐标
// 0, 0, 100, //顶点5坐标
// 0, 0, -10, //顶点6坐标
//]);
// 创建属性缓冲区对象 3个为一组,表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices,3);
// 设置几何体attributes属性的位置属性
//geometry.attributes.position = attribue;
pointsGeometry = new THREE.BufferGeometry().setFromPoints(snowflakePoints)
//pointsGeometry.attributes.position // 几何体顶点位置
//pointsGeometry.attributes.color // 几何体顶点颜色
//pointsGeometry.attributes.normal // 几何体顶点法向量
//pointsGeometry.attributes.uv // 几何体顶点像素
// 计算边界球体
pointsGeometry.computeBoundingSphere();
// 创建雪花的材质和网格
//let pointsMaterial = new THREE.LineBasicMaterial({color: 0xffffff,opacity: 1.0,linewidth: 1 });
//snowflakePoint = new THREE.LineSegments(pointsGeometry, pointsMaterial);
//点模型对象
pointsMaterial = new THREE.PointsMaterial({color:params.color,size:0.01, opacity: Math.random(),})
snowflakePoint = new THREE.Points(pointsGeometry, pointsMaterial);
snowflakePoint.name = 'points';
//const snowflakeMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff,side: THREE.DoubleSide });
//MeshBasicMaterial 不受光照的影响
//MeshLambertMaterial 只在顶点计算光照
//MeshPhongMaterial 在每个像素计算光照 支持镜面高光
//snowflakePoint = new THREE.Mesh(pointsGeometry, snowflakeMaterial);
snowflakePoint.position.set( 0, 1, 0 );
//snowflake.rotation.set( 0, 1, 0 );
snowflakePoint.scale.set( params.length, params.length, params.length );
// 几何体绕着x轴旋转90度
snowflakePoint.rotateX(Math.PI / 2);
snowflakePoint.castShadow = true;
//console.log(snowflakePoint,vertices)
return snowflakePoint;
}
setScene();
</script>
</body>
</html>
参见: