👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️THREE.ShaderMaterial](#1.1 ☘️THREE.ShaderMaterial)
-
- [1.1.1 ☘️注意事项](#1.1.1 ☘️注意事项)
- [1.1.2 ☘️构造函数](#1.1.2 ☘️构造函数)
- [1.1.3 ☘️属性](#1.1.3 ☘️属性)
- [1.1.4 ☘️方法](#1.1.4 ☘️方法)
- [1.2 ☘️THREE.EffectComposer 后期处理](#1.2 ☘️THREE.EffectComposer 后期处理)
-
- [1.2.1 ☘️代码示例](#1.2.1 ☘️代码示例)
- [1.2.2 ☘️构造函数](#1.2.2 ☘️构造函数)
- [1.2.3 ☘️属性](#1.2.3 ☘️属性)
- [1.2.4 ☘️方法](#1.2.4 ☘️方法)
- [1.3 ☘️THREE.RenderPass](#1.3 ☘️THREE.RenderPass)
-
- [1.3.1 ☘️构造函数](#1.3.1 ☘️构造函数)
- [1.3.2 ☘️属性](#1.3.2 ☘️属性)
- [1.3.3 ☘️方法](#1.3.3 ☘️方法)
- [1.4 ☘️THREE.UnrealBloomPass](#1.4 ☘️THREE.UnrealBloomPass)
-
- [1.4.1 ☘️构造函数](#1.4.1 ☘️构造函数)
- [1.4.2 ☘️方法](#1.4.2 ☘️方法)
- 二、🍀实现山谷奔跑效果
-
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#2. ☘️代码样例)
一、🍀前言
本文详细介绍如何基于threejs在三维场景中实现山谷奔跑效果,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️THREE.ShaderMaterial
THREE.ShaderMaterial使用自定义shader渲染的材质。 shader是一个用GLSL编写的小程序 ,在GPU上运行。
1.1.1 ☘️注意事项
- ShaderMaterial 只有使用 WebGLRenderer 才可以绘制正常, 因为 vertexShader 和
fragmentShader 属性中GLSL代码必须使用WebGL来编译并运行在GPU中。 - 从 THREE r72开始,不再支持在ShaderMaterial中直接分配属性。 必须使用
BufferGeometry实例,使用BufferAttribute实例来定义自定义属性。 - 从 THREE r77开始,WebGLRenderTarget 或 WebGLCubeRenderTarget
实例不再被用作uniforms。 必须使用它们的texture 属性。 - 内置attributes和uniforms与代码一起传递到shaders。
如果您不希望WebGLProgram向shader代码添加任何内容,则可以使用RawShaderMaterial而不是此类。 - 您可以使用指令#pragma unroll_loop_start,#pragma unroll_loop_end
以便通过shader预处理器在GLSL中展开for循环。 该指令必须放在循环的正上方。循环格式必须与定义的标准相对应。 - 循环必须标准化normalized。
- 循环变量必须是i。
- 对于给定的迭代,值 UNROLLED_LOOP_INDEX 将替换为 i 的显式值,并且可以在预处理器语句中使用。
javascript
#pragma unroll_loop_start
for ( int i = 0; i < 10; i ++ ) {
// ...
}
#pragma unroll_loop_end
代码示例
javascript
const material = new THREE.ShaderMaterial( {
uniforms: {
time: { value: 1.0 },
resolution: { value: new THREE.Vector2() }
},
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent
} );
1.1.2 ☘️构造函数
ShaderMaterial( parameters : Object )
parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。 材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。
1.1.3 ☘️属性
共有属性请参见其基类Material。
.clipping : Boolean
定义此材质是否支持剪裁; 如果渲染器传递clippingPlanes uniform,则为true。默认值为false。
.defaultAttributeValues : Object
当渲染的几何体不包含这些属性但材质包含这些属性时,这些默认值将传递给shaders。这可以避免在缓冲区数据丢失时出错。
javascript
this.defaultAttributeValues = {
'color': [ 1, 1, 1 ],
'uv': [ 0, 0 ],
'uv2': [ 0, 0 ]
};
.defines : Object
使用 #define 指令在GLSL代码为顶点着色器和片段着色器定义自定义常量;每个键/值对产生一行定义语句:
javascript
defines: {
FOO: 15,
BAR: true
}
这将在GLSL代码中产生如下定义语句:
javascript
#define FOO 15
#define BAR true
.extensions : Object
一个有如下属性的对象:
javascript
this.extensions = {
derivatives: false, // set to use derivatives
fragDepth: false, // set to use fragment depth values
drawBuffers: false, // set to use draw buffers
shaderTextureLOD: false // set to use shader texture LOD
};
.fog : Boolean
定义材质颜色是否受全局雾设置的影响; 如果将fog uniforms传递给shader,则为true。默认值为false。
.fragmentShader : String
片元着色器的GLSL代码。这是shader程序的实际代码。在上面的例子中, vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。
.glslVersion : String
定义自定义着色器代码的 GLSL 版本。仅与 WebGL 2 相关,以便定义是否指定 GLSL 3.0。有效值为 THREE.GLSL1 或 THREE.GLSL3。默认为空。
.index0AttributeName : String
如果设置,则调用gl.bindAttribLocation 将通用顶点索引绑定到属性变量。默认值未定义。
.isShaderMaterial : Boolean
只读标志,用于检查给定对象是否属于 ShaderMaterial 类型。
.lights : Boolean
材质是否受到光照的影响。默认值为 false。如果传递与光照相关的uniform数据到这个材质,则为true。默认是false。
.linewidth : Float
控制线框宽度。默认值为1。
由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。
.flatShading : Boolean
定义材质是否使用平面着色进行渲染。默认值为false。
.uniforms : Object
如下形式的对象:
javascript
{ "uniform1": { value: 1.0 }, "uniform2": { value: 2 } }
指定要传递给shader代码的uniforms;键为uniform的名称,值(value)是如下形式:
javascript
{ value: 1.0 }
这里 value 是uniform的值。名称必须匹配 uniform 的name,和GLSL代码中的定义一样。 注意,uniforms逐帧被刷新,所以更新uniform值将立即更新GLSL代码中的相应值。
.uniformsNeedUpdate : Boolean
可用于在 Object3D.onBeforeRender() 中更改制服时强制进行制服更新。默认为假。
.vertexColors : Boolean
定义是否使用顶点着色。默认为假。
.vertexShader : String
顶点着色器的GLSL代码。这是shader程序的实际代码。 在上面的例子中,vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。
.wireframe : Boolean
将几何体渲染为线框(通过GL_LINES而不是GL_TRIANGLES)。默认值为false(即渲染为平面多边形)。
.wireframeLinewidth : Float
控制线框宽度。默认值为1。
由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。
1.1.4 ☘️方法
共有方法请参见其基类Material。
.clone () : ShaderMaterial this : ShaderMaterial
创建该材质的一个浅拷贝。需要注意的是,vertexShader和fragmentShader使用引用拷贝; attributes的定义也是如此; 这意味着,克隆的材质将共享相同的编译WebGLProgram; 但是,uniforms 是 值拷贝,这样对不同的材质我们可以有不同的uniforms变量。
1.2 ☘️THREE.EffectComposer 后期处理
THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。
1.2.1 ☘️代码示例
javascript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 初始化 composer
const composer = new EffectComposer(renderer);
// 创建 RenderPass 并添加到 composer
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加其他后期处理通道(如模糊)
// composer.addPass(blurPass);
// 在动画循环中渲染
function animate() {
composer.render();
requestAnimationFrame(animate);
}
1.2.2 ☘️构造函数
EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer -- 用于渲染场景的渲染器。
renderTarget -- (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
1.2.3 ☘️属性
.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。
javascript
渲染通道:
BloomPass 该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass 将一层黑点贴到代表原始图片的屏幕上
FilmPass 通过扫描线和失真模拟电视屏幕
MaskPass 在当前图片上贴一层掩膜,后续通道只会影响被贴的区域
RenderPass 该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass 执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass 使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数
.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。
.renderer : WebGLRenderer
内部渲染器的引用。
.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。
.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。
1.2.4 ☘️方法
.addPass ( pass : Pass ) : undefined
pass -- 将被添加到过程链的过程
将传入的过程添加到过程链。
.dispose () : undefined
释放此实例分配的 GPU 相关资源。每当您的应用程序不再使用此实例时调用此方法。
.insertPass ( pass : Pass, index : Integer ) : undefined
pass -- 将被插入到过程链的过程。
index -- 定义过程链中过程应插入的位置。
将传入的过程插入到过程链中所给定的索引处。
.isLastEnabledPass ( passIndex : Integer ) : Boolean
passIndex -- 被用于检查的过程
如果给定索引的过程在过程链中是最后一个启用的过程,则返回true。 由EffectComposer所使用,来决定哪一个过程应当被渲染到屏幕上。
.removePass ( pass : Pass ) : undefined
pass -- 要从传递链中删除的传递。
从传递链中删除给定的传递。
.render ( deltaTime : Float ) : undefined
deltaTime -- 增量时间值。
执行所有启用的后期处理过程,来产生最终的帧,
.reset ( renderTarget : WebGLRenderTarget ) : undefined
renderTarget -- (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
重置所有EffectComposer的内部状态。
.setPixelRatio ( pixelRatio : Float ) : undefined
pixelRatio -- 设备像素比
设置设备的像素比。该值通常被用于HiDPI设备,以阻止模糊的输出。 因此,该方法语义类似于WebGLRenderer.setPixelRatio()。
.setSize ( width : Integer, height : Integer ) : undefined
width -- EffectComposer的宽度。
height -- EffectComposer的高度。
考虑设备像素比,重新设置内部渲染缓冲和过程的大小为(width, height)。 因此,该方法语义类似于WebGLRenderer.setSize()。
.swapBuffers () : undefined
交换内部的读/写缓冲。
1.3 ☘️THREE.RenderPass
THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。
1.3.1 ☘️构造函数
RenderPass(scene, camera, overrideMaterial, clearColor, clearAlpha)
- scene THREE.Scene 要渲染的 Three.js 场景对象。
- camera THREE.Camera 场景对应的相机(如 PerspectiveCamera)。
- overrideMaterial THREE.Material (可选) 覆盖场景中所有物体的材质(默认 null)。
- clearColor THREE.Color (可选) 渲染前清除画布的颜色(默认不主动清除)。
- clearAlpha number (可选) 清除画布的透明度(默认 0)。
1.3.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。
.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。
.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。
1.3.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
1.4 ☘️THREE.UnrealBloomPass
UnrealBloomPass 是 Three.js 中实现高质量泛光效果的后期处理通道,通过模拟类似 Unreal Engine 的泛光效果,为场景中的明亮区域添加柔和的光晕,提升视觉表现力。
1.4.1 ☘️构造函数
new UnrealBloomPass(resolution, strength, radius, threshold)
- resolution (Vector2): 泛光效果应用的场景分辨率,需与画布尺寸一致。
示例:new THREE.Vector2(window.innerWidth, window.innerHeight) - strength (Number): 泛光强度,默认值 1.0。值越大,光晕越明显。
- radius (Number): 模糊半径,默认值 0.4。值越大,光晕扩散范围越广。
- threshold (Number): 泛光阈值,默认值 0.85。仅对亮度高于此值的区域生效。
1.4.2 ☘️方法
- renderToScreen: 是否直接渲染到屏幕,默认为 false(需通过 EffectComposer 管理)。
- clearColor: 设置背景清除颜色,默认为透明。
二、🍀实现山谷奔跑效果
1. ☘️实现思路
通过threejs的ShaderMaterial自定义着色器、UnrealBloomPass高质量泛光效果后期处理通道实现山谷奔跑效果。具体代码参考下面代码样例。
2. ☘️代码样例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>山谷奔跑</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/168886/RenderPass.110.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/168886/EffectComposer.110.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/168886/UnrealBloomPass.110.js"></script>
</head>
<style>
body {
color: #ffffff;
font-family: Monospace;
font-size: 13px;
text-align: center;
font-weight: bold;
background-color: #000;
margin: 0px;
overflow: hidden;
}
#info,
#controls {
position: absolute;
width: 100%;
padding: 5px;
background: rgba(0, 0, 0, 0.4);
}
#info {
top: 0;
}
#controls {
bottom: 0;
}
a {
color: #ffffff;
transition: 150ms all;
}
a:hover, a:focus {
color: #ffc107;
}
.stats-element {
position: absolute;
right: 0;
top: 0;
}
.audio-embed {
position: absolute;
left: 0;
bottom: 100%;
border: 0;
}
#hide-audio:checked + .audio-embed {
opacity: 0;
}
</style>
<body>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec3 center;
uniform float uTime;
varying float vDisp;
varying vec3 vCenter;
varying vec2 vSceneYZ;
#define PULSE_TIME 1.16
void main() {
vCenter = center;
vDisp = max(
max(0., 1.-pow(3.*abs(uv.y-fract(-uTime*PULSE_TIME)+0.5),0.5)),
1.-pow(3.*abs(uv.y-fract(-uTime*PULSE_TIME)-0.5),0.5)
);
// FIXME - magic numbers in displacement calculation
vec4 scenePosition = modelViewMatrix*vec4(position+vec3(0.,1.,0.)*2.5*vDisp,1.);
vSceneYZ = scenePosition.yz;
gl_Position = projectionMatrix*scenePosition;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
uniform float uResolutionScale;
varying float vDisp;
varying vec3 vCenter;
varying vec2 vSceneYZ;
#define PI 3.14159265359
#define WIREFRAME_WIDTH 2.5
// adapted from https://github.com/mrdoob/three.js/blob/dev/examples/webgl_materials_wireframe.html for wireframe effect
float edgeFactorTri() {
vec3 a3 = smoothstep(vec3(0.), fwidth(vCenter.xyz)*WIREFRAME_WIDTH/uResolutionScale, vCenter.xyz);
return min(min(a3.x, a3.y), a3.z);
}
void main( void ) {
if (edgeFactorTri() > 0.98) discard;
vec3 color = mix(
mix(
mix(
vec3(1.,0.,0.6), // magenta base
vec3(1., 0.9, .0), min(1.9,vDisp) // yellow pulse
),
vec3(1.), max(0., (vSceneYZ.s-20.) / 120.) // lighter on top; FIXME - magic numbers with Y position
),
vec3(0.), max(0., min(1., (-vSceneYZ.t - 80.) / 80.)) // fade to black; FIXME - magic numbers with Z position
);
gl_FragColor = gl_FrontFacing ?
vec4(color, 1.0) :
vec4(color, 0.5);
}
</script>
<div id="container"></div>
<div id="info">Synth Canyon Run - with <a href="https://threejs.org" target="_blank">three.js</a>
</div>
<div id="controls">
<label for="resolution">resolution: </label>
<select id="resolution" value="2">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
<option value="4">4x</option>
<option value="8">8x</option>
</select>
<label for="hide-audio">hide audio: </label>
<input id="hide-audio" type="checkbox"></input>
<iframe class="audio-embed" width="350" height="83" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/450662742&color=%23a575d0&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true"></iframe>
</div>
</body>
<script>
const SHOW_STATS = false;
const CANYON_WIDTH = 400;
const CANYON_LENGTH = 120;
const CANYON_SEGMENTS_W = 27;
const CANYON_SEGMENTS_L = 10;
const CLIFF_BASE = 60;
const CLIFF_VARY = 15;
const FLOOR_VARY = 10;
const CANYON_SPEED = 70;
const CAMERA_DRIFT_DISTANCE = 15;
const CAMERA_DRIFT_SPEED = 0.05;
let lastUpdate;
let camera, scene, renderer, composer;
let cameraBaseX, cameraBaseY;
let uResolutionScale;
let uTime;
let canyonA, canyonB;
function init() {
// stats
if (SHOW_STATS) {
const stats = new Stats();
stats.domElement.classList.add('stats-element');
document.body.appendChild(stats.domElement);
requestAnimationFrame(function updateStats(){
stats.update();
requestAnimationFrame(updateStats);
});
}
// basic setup
const container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 300);
const cameraDistance = 70;
const cameraAngle = .05*Math.PI;
camera.position.z = cameraDistance;
cameraBaseX = 0;
cameraBaseY = 0.3 * (CLIFF_BASE + CLIFF_VARY + FLOOR_VARY);
camera.position.y = cameraBaseY;
camera.rotation.x = -cameraAngle;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0x000000, 1.0);
renderer.setPixelRatio(window.devicePixelRatio);
// shader setup
lastUpdate = new Date().getTime();
const vertexShader = document.getElementById( 'vertexShader' ).textContent;
const fragmentShader = document.getElementById( 'fragmentShader' ).textContent;
uTime = { type: 'f', value: 1.0 };
uResolutionScale = { type: 'f', value: 1.0 };
// add objects
const canyonGeometry = new THREE.PlaneGeometry(CANYON_WIDTH, CANYON_LENGTH, CANYON_SEGMENTS_W, CANYON_SEGMENTS_L);
canyonGeometry.rotateX(-0.5 * Math.PI);
const reverseGeometry = canyonGeometry.clone();
const simplexA = new SimplexNoise(Math.floor(0xffff*Math.random()));
const simplexB = new SimplexNoise(Math.floor(0xffff*Math.random()));
for (let i = 0, l = canyonGeometry.vertices.length; i < l; i++) {
const { x, z } = canyonGeometry.vertices[i];
canyonGeometry.vertices[i].y =
Math.min(1.0, Math.pow(x/50, 4)) * Math.round(CLIFF_BASE + simplexA.noise2D(x,z) * CLIFF_VARY) + Math.round(simplexB.noise2D(x,z) * FLOOR_VARY);
reverseGeometry.vertices[i].y =
Math.min(1.0, Math.pow(x/50, 4)) * Math.round(CLIFF_BASE + simplexA.noise2D(x,-z) * CLIFF_VARY) + Math.round(simplexB.noise2D(x,-z) * FLOOR_VARY);
}
const canyonMaterial = new THREE.ShaderMaterial({
transparent: true,
side: THREE.DoubleSide,
uniforms: { uTime, uResolutionScale },
vertexShader,
fragmentShader
});
canyonMaterial.extensions.derivatives = true;
canyonA = new THREE.Mesh(geomToBufferGeomWithCenters(canyonGeometry), canyonMaterial);
scene.add(canyonA);
canyonB = new THREE.Mesh(geomToBufferGeomWithCenters(reverseGeometry), canyonMaterial);
canyonB.position.z -= CANYON_LENGTH;
scene.add(canyonB);
container.appendChild(renderer.domElement);
// effect composition
const renderScene = new THREE.RenderPass(scene, camera);
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.86);
bloomPass.threshold = 0.3;
bloomPass.strength = 2.5 * uResolutionScale.value;
bloomPass.radius = 0.3 * uResolutionScale.value;
composer = new THREE.EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// event listeners
onWindowResize();
window.addEventListener( 'resize', onWindowResize, false);
document.getElementById('resolution').addEventListener('change', onResolutionChange, false);
}
// events
function onWindowResize(evt) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function onResolutionChange(evt) {
uResolutionScale.value = parseFloat(evt.target.value);
bloomPass.strength = 2.5 * uResolutionScale.value;
bloomPass.radius = 0.3 * uResolutionScale.value;
renderer.setPixelRatio( window.devicePixelRatio / uResolutionScale.value );
}
function animate() {
const currentTime = new Date().getTime();
const timeSinceLastUpdate = currentTime - lastUpdate;
lastUpdate = currentTime;
const deltaTime = timeSinceLastUpdate / 1000;
uTime.value += deltaTime;
// move canyons
canyonA.position.z += deltaTime * CANYON_SPEED;
canyonB.position.z += deltaTime * CANYON_SPEED;
if (canyonA.position.z > CANYON_LENGTH) {
canyonA.position.z -= 2*CANYON_LENGTH;
}
if (canyonB.position.z > CANYON_LENGTH) {
canyonB.position.z -= 2*CANYON_LENGTH;
}
// drift camera (simple lissajous)
camera.position.x = cameraBaseX + CAMERA_DRIFT_DISTANCE*Math.sin(7*CAMERA_DRIFT_SPEED*Math.PI*uTime.value);
camera.position.y = cameraBaseY + CAMERA_DRIFT_DISTANCE*Math.sin(5*CAMERA_DRIFT_SPEED*Math.PI*uTime.value);
// render
// renderer.render( scene, camera );
composer.render();
requestAnimationFrame( animate );
}
// boot
init();
animate();
// utils
// adapted from https://github.com/mrdoob/three.js/blob/dev/examples/webgl_materials_wireframe.html for wireframe effect
function geomToBufferGeomWithCenters(geom) {
const buffGeom = new THREE.BufferGeometry().fromGeometry(geom);
const vectors = [new THREE.Vector3(1,0,0), new THREE.Vector3(0,1,0), new THREE.Vector3(0,0,1)];
const { position } = buffGeom.attributes;
const centers = new Float32Array(position.count*3);
for (let i=0, l=position.count; i<l; i++) {
vectors[i%3].toArray(centers,i*3);
}
buffGeom.setAttribute('center', new THREE.BufferAttribute(centers,3));
return buffGeom;
}
</script>
</html>
效果如下:

源码