1. 黏性流体雕刻:数字橡皮泥的交互实现
1.1 核心交互机制解析
数字橡皮泥的核心在于模拟真实黏土的物理特性。当用户点击并拖拽3D物体表面时,物体需要像真实黏土一样产生形变,并在松开后保持变形状态。这种交互方式的关键在于:
- 形变区域控制:形变不应影响整个物体,而是局限在点击点周围特定半径范围内
- 形变平滑过渡:形变区域的边缘需要平滑过渡,避免出现明显的接缝或棱角
- 拓扑保持:形变后的物体表面拓扑结构需要保持完整,不能出现破面或自相交
实现这种效果最有效的方法是使用有符号距离场(SDF)技术。SDF可以精确描述物体表面到空间各点的距离,通过修改SDF值就能自然地改变物体形状,同时保持拓扑完整性。
1.2 技术实现细节
1.2.1 基础场景搭建
首先使用Three.js创建一个基础场景和可交互的球体:
javascript复制// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 创建可交互球体
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshStandardMaterial({
color: 0x6699cc,
roughness: 0.3,
metalness: 0.7
});
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
1.2.2 交互逻辑实现
实现鼠标拖拽形变的核心代码如下:
javascript复制// 射线检测设置
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseMove(event) {
// 计算鼠标位置归一化坐标
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 更新射线
raycaster.setFromCamera(mouse, camera);
// 检测与球体的交点
const intersects = raycaster.intersectObject(sphere);
if (intersects.length > 0 && isDragging) {
// 获取交点信息
const point = intersects[0].point;
const face = intersects[0].face;
// 应用形变力场
applyDeformationField(point, face);
}
}
// 高斯衰减函数
function gaussian(x, sigma) {
return Math.exp(-(x * x) / (2 * sigma * sigma));
}
1.2.3 形变力场应用
形变力场的实现需要考虑以下几个关键点:
- 影响半径:确定形变影响的范围,通常使用物理单位或相对于物体大小的比例
- 衰减函数:使用高斯函数确保形变强度随距离平滑衰减
- 顶点位移:只修改影响半径内的顶点位置
javascript复制function applyDeformationField(centerPoint, centerFace) {
// 获取几何体属性
const positionAttribute = sphere.geometry.attributes.position;
const vertex = new THREE.Vector3();
// 遍历所有顶点
for (let i = 0; i < positionAttribute.count; i++) {
vertex.fromBufferAttribute(positionAttribute, i);
// 计算顶点到形变中心的距离
const distance = vertex.distanceTo(centerPoint);
// 如果在影响半径内
if (distance < DEFORMATION_RADIUS) {
// 计算衰减因子
const attenuation = gaussian(distance / DEFORMATION_RADIUS, 0.5);
// 计算位移方向
const direction = new THREE.Vector3().subVectors(
mousePosition3D,
centerPoint
).normalize();
// 应用位移
vertex.add(direction.multiplyScalar(DEFORMATION_STRENGTH * attenuation));
// 更新顶点位置
positionAttribute.setXYZ(i, vertex.x, vertex.y, vertex.z);
}
}
// 更新法线
sphere.geometry.computeVertexNormals();
positionAttribute.needsUpdate = true;
}
1.3 着色器优化与性能考量
为了实现更流畅的交互体验,我们可以使用着色器来优化形变效果:
glsl复制// 顶点着色器
uniform vec3 u_deformationCenter;
uniform float u_deformationRadius;
uniform float u_deformationStrength;
uniform vec3 u_deformationDirection;
void main() {
vec3 displacedPosition = position;
float distanceToCenter = distance(position, u_deformationCenter);
if (distanceToCenter < u_deformationRadius) {
float attenuation = exp(-(distanceToCenter*distanceToCenter)/(2.0*u_deformationRadius*u_deformationRadius));
displacedPosition += u_deformationDirection * u_deformationStrength * attenuation;
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0);
}
这种实现方式的优势在于:
- 性能更高:形变计算在GPU上完成,减轻CPU负担
- 效果更平滑:避免了JavaScript中逐顶点计算的性能瓶颈
- 实时响应:可以处理更高精度的模型
重要提示:在实现形变效果时,务必注意更新法线向量。未正确更新的法线会导致光照计算错误,使表面看起来不自然。可以使用
geometry.computeVertexNormals()或在着色器中实时计算法线。
2. 电磁场可视化:感应电力交互系统
2.1 电磁场模拟原理
电磁场可视化系统的核心是模拟点电荷产生的电场分布。根据库仑定律,点电荷产生的电场强度可以表示为:
$$
\vec{E} = k_e \frac{q}{r^2} \hat{r}
$$
其中:
- $k_e$ 是库仑常数
- $q$ 是点电荷量
- $r$ 是到点电荷的距离
- $\hat{r}$ 是从点电荷指向观察点的单位向量
在实现中,我们需要:
- 定义场景中的电荷点(正负电荷)
- 计算空间各点的电场强度
- 可视化电场线或使用粒子表示场强
2.2 技术实现方案
2.2.1 场景设置与电荷定义
javascript复制// 定义电荷类
class Charge {
constructor(position, chargeValue) {
this.position = position;
this.charge = chargeValue; // 正值表示正电荷,负值表示负电荷
this.mesh = this.createVisualization();
}
createVisualization() {
const geometry = new THREE.SphereGeometry(0.1, 32, 32);
const material = new THREE.MeshBasicMaterial({
color: this.charge > 0 ? 0xff0000 : 0x0000ff
});
return new THREE.Mesh(geometry, material);
}
}
// 创建电荷系统
const charges = [
new Charge(new THREE.Vector3(1, 0, 0), 1), // 正电荷
new Charge(new THREE.Vector3(-1, 0, 0), -1) // 负电荷
];
// 将电荷添加到场景
charges.forEach(charge => scene.add(charge.mesh));
2.2.2 电场计算与粒子系统
javascript复制// 创建粒子系统
const particleCount = 1000;
const particles = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
// 初始化粒子位置
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 10;
positions[i3 + 1] = (Math.random() - 0.5) * 10;
positions[i3 + 2] = (Math.random() - 0.5) * 10;
}
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particles.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 粒子材质
const particleMaterial = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true,
transparent: true,
opacity: 0.8
});
const particleSystem = new THREE.Points(particles, particleMaterial);
scene.add(particleSystem);
2.2.3 实时场强计算与更新
javascript复制function calculateFieldStrength(position) {
let totalField = new THREE.Vector3(0, 0, 0);
charges.forEach(charge => {
const direction = new THREE.Vector3().subVectors(
position,
charge.position
);
const distance = direction.length();
direction.normalize();
// 库仑定律计算
const fieldMagnitude = COULOMB_CONST * charge.charge / (distance * distance);
const fieldVector = direction.multiplyScalar(fieldMagnitude);
totalField.add(fieldVector);
});
return totalField;
}
function updateParticles() {
const positions = particles.attributes.position.array;
const colors = particles.attributes.color.array;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
const position = new THREE.Vector3(
positions[i3],
positions[i3 + 1],
positions[i3 + 2]
);
// 计算场强
const field = calculateFieldStrength(position);
const fieldStrength = field.length();
// 更新粒子位置(沿场线移动)
field.normalize().multiplyScalar(0.01);
positions[i3] += field.x;
positions[i3 + 1] += field.y;
positions[i3 + 2] += field.z;
// 根据场强设置颜色
const hue = 0.6 - Math.min(fieldStrength * 0.1, 0.6);
const rgb = new THREE.Color().setHSL(hue, 1.0, 0.5);
colors[i3] = rgb.r;
colors[i3 + 1] = rgb.g;
colors[i3 + 2] = rgb.b;
}
particles.attributes.position.needsUpdate = true;
particles.attributes.color.needsUpdate = true;
}
2.3 交互功能实现
2.3.1 探针交互逻辑
javascript复制// 创建探针
const probeGeometry = new THREE.SphereGeometry(0.05, 16, 16);
const probeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const probe = new THREE.Mesh(probeGeometry, probeMaterial);
scene.add(probe);
// 探针跟随鼠标
function updateProbePosition(event) {
// 将鼠标坐标转换为3D空间坐标
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([groundPlane]);
if (intersects.length > 0) {
probe.position.copy(intersects[0].point);
// 计算并显示当前位置的场强
const field = calculateFieldStrength(probe.position);
updateFieldDisplay(field);
}
}
2.3.2 场强可视化优化
为了更直观地展示场强分布,我们可以实现以下优化:
- 场线绘制:从正电荷出发,沿电场方向绘制场线
- 等势面可视化:使用半透明几何体表示相同电势的区域
- 动态粒子密度:在场强较大区域增加粒子密度
javascript复制// 场线绘制函数
function drawFieldLines() {
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
// 从每个正电荷出发绘制10条场线
charges.filter(c => c.charge > 0).forEach(charge => {
for (let i = 0; i < 10; i++) {
const points = [];
let currentPos = charge.position.clone();
points.push(currentPos.clone());
// 沿电场方向追踪
for (let step = 0; step < 100; step++) {
const field = calculateFieldStrength(currentPos);
if (field.length() < 0.01) break; // 场强太小则停止
field.normalize().multiplyScalar(0.1);
currentPos.add(field);
points.push(currentPos.clone());
}
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line);
}
});
}
性能提示:场线计算可能很耗性能,可以考虑以下优化策略:
- 使用Web Worker在后台线程计算场线路径
- 限制场线数量和长度
- 使用LOD(细节层次)技术,根据相机距离调整场线精度
3. 过程化城市增长:波函数折叠算法应用
3.1 波函数折叠算法原理
波函数折叠(Wave Function Collapse, WFC)算法是一种过程化生成技术,其核心思想是:
- 可能性空间:每个网格单元开始时包含所有可能的模块(如建筑类型)
- 观察与坍缩:选择一个熵最低(可能性最少)的单元,随机选择其一个可能状态
- 传播约束:根据相邻规则,限制相邻单元的可能性
- 迭代:重复上述过程直到所有单元都坍缩为确定状态
在城市生成场景中,我们可以简化WFC算法:
- 模块:不同类型的建筑(住宅、商业、工业等)
- 相邻规则:道路必须连接道路,建筑不能重叠等
3.2 技术实现细节
3.2.1 基础场景设置
javascript复制// 定义城市网格
const citySize = 20;
const cityGrid = Array(citySize).fill().map(
() => Array(citySize).fill(null)
);
// 建筑类型定义
const buildingTypes = [
{ name: 'residential', size: 1, color: 0x88ccee },
{ name: 'commercial', size: 1, color: 0xee8866 },
{ name: 'industrial', size: 2, color: 0xaaaaaa }
];
// 地面网格
const groundGeometry = new THREE.PlaneGeometry(citySize, citySize, citySize, citySize);
const groundMaterial = new THREE.MeshBasicMaterial({
color: 0x333333,
wireframe: true
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
3.2.2 建筑生长逻辑
javascript复制// 点击事件处理
function handleCityClick(event) {
// 获取点击位置
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(ground);
if (intersects.length > 0) {
// 转换为网格坐标
const point = intersects[0].point;
const gridX = Math.floor(point.x + citySize/2);
const gridZ = Math.floor(point.z + citySize/2);
// 检查位置是否可用
if (gridX >= 0 && gridX < citySize && gridZ >= 0 && gridZ < citySize && !cityGrid[gridX][gridZ]) {
// 随机选择建筑类型
const typeIndex = Math.floor(Math.random() * buildingTypes.length);
const buildingType = buildingTypes[typeIndex];
// 创建建筑
createBuilding(gridX, gridZ, buildingType);
// 尝试连接道路
attemptRoadConnection(gridX, gridZ);
}
}
}
// 创建建筑函数
function createBuilding(x, z, type) {
const height = 1 + Math.random() * type.size;
const geometry = new THREE.BoxGeometry(type.size, height, type.size);
const material = new THREE.MeshStandardMaterial({
color: type.color,
metalness: 0.3,
roughness: 0.7
});
const building = new THREE.Mesh(geometry, material);
building.position.set(
x - citySize/2 + type.size/2,
height/2,
z - citySize/2 + type.size/2
);
scene.add(building);
cityGrid[x][z] = { type: type.name, mesh: building };
// 添加生长动画
animateBuildingGrowth(building, height);
}
3.2.3 道路连接算法
javascript复制function attemptRoadConnection(x, z) {
// 检查四个相邻方向
const directions = [
{ dx: 1, dz: 0 }, // 右
{ dx: -1, dz: 0 }, // 左
{ dx: 0, dz: 1 }, // 上
{ dx: 0, dz: -1 } // 下
];
// 随机顺序检查相邻单元格
shuffleArray(directions).forEach(dir => {
const nx = x + dir.dx;
const nz = z + dir.dz;
// 检查相邻单元格是否在范围内且有建筑
if (nx >= 0 && nx < citySize && nz >= 0 && nz < citySize && cityGrid[nx][nz]) {
// 检查是否已经有连接
if (!areConnected(x, z, nx, nz)) {
// 创建道路
createRoad(x, z, nx, nz);
}
}
});
}
function createRoad(x1, z1, x2, z2) {
// 计算道路中心点和长度
const start = new THREE.Vector3(
x1 - citySize/2 + 0.5,
0.01,
z1 - citySize/2 + 0.5
);
const end = new THREE.Vector3(
x2 - citySize/2 + 0.5,
0.01,
z2 - citySize/2 + 0.5
);
const center = new THREE.Vector3().lerpVectors(start, end, 0.5);
const length = start.distanceTo(end);
// 创建道路几何体
const roadGeometry = new THREE.PlaneGeometry(length, 0.3);
const roadMaterial = new THREE.MeshStandardMaterial({
color: 0x444444,
emissive: 0x222222,
emissiveIntensity: 0.5
});
const road = new THREE.Mesh(roadGeometry, roadMaterial);
road.position.copy(center);
road.lookAt(end);
road.rotation.x = -Math.PI / 2;
scene.add(road);
// 添加连接记录
addConnection(x1, z1, x2, z2);
}
3.3 动画与视觉效果优化
3.3.1 建筑生长动画
javascript复制function animateBuildingGrowth(building, targetHeight) {
const startTime = Date.now();
const duration = 1000; // 1秒动画
function update() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// 弹性缩放函数
const scale = elasticOut(progress) * targetHeight;
building.scale.y = scale / targetHeight;
building.position.y = scale / 2;
if (progress < 1) {
requestAnimationFrame(update);
}
}
update();
}
// 弹性缓动函数
function elasticOut(t) {
return Math.sin(-13 * (t + 1) * Math.PI/2) * Math.pow(2, -10 * t) + 1;
}
3.3.2 数字光路效果
javascript复制function createLightPath(start, end) {
// 创建光路曲线
const curve = new THREE.CatmullRomCurve3([
start,
new THREE.Vector3(
(start.x + end.x) / 2,
1 + Math.random() * 0.5,
(start.z + end.z) / 2
),
end
]);
// 采样曲线点
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// 创建线材质
const material = new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.7,
linewidth: 2
});
const line = new THREE.Line(geometry, material);
scene.add(line);
// 添加脉冲动画
animateLightPulse(line);
}
function animateLightPulse(line) {
let phase = 0;
function update() {
phase += 0.05;
const intensity = 0.5 + 0.5 * Math.sin(phase);
line.material.opacity = intensity * 0.7;
line.material.needsUpdate = true;
requestAnimationFrame(update);
}
update();
}
设计建议:为了增强赛博朋克风格,可以考虑添加以下元素:
- 霓虹灯光效果
- 全息广告牌
- 低空飞行的飞行器
- 环境雾效和体积光
这些元素可以通过Three.js的后处理效果和粒子系统实现
4. 镜像空间:平行维度的实现技术
4.1 镜像空间的核心原理
镜像空间的实现需要解决几个关键技术问题:
- 实时反射:镜面需要准确反射场景中的物体
- 维度切换:物体穿过镜面时需要改变属性和行为
- 视觉效果:两个维度的视觉风格需要明显区分
传统平面镜反射可以使用Three.js的CubeCamera实现,但平行维度的概念需要更复杂的处理:
- 维护两个独立场景:现实场景和镜像场景
- 使用渲染目标(RenderTarget)实现镜面效果
- 物体穿过镜面时在逻辑上切换到另一个场景
4.2 技术实现方案
4.2.1 场景与相机设置
javascript复制// 主场景(现实维度)
const mainScene = new THREE.Scene();
mainScene.background = new THREE.Color(0x222222);
// 镜像场景(平行维度)
const mirrorScene = new THREE.Scene();
mirrorScene.background = new THREE.Color(0x111133);
// 创建镜面
const mirrorSize = 5;
const mirrorGeometry = new THREE.PlaneGeometry(mirrorSize, mirrorSize);
const mirrorMaterial = new THREE.MeshBasicMaterial({
color: 0x333355,
side: THREE.DoubleSide
});
const mirror = new THREE.Mesh(mirrorGeometry, mirrorMaterial);
mirror.rotation.y = Math.PI;
mainScene.add(mirror);
// 创建渲染目标用于镜面反射
const renderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{ minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter }
);
// 镜像相机
const mirrorCamera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
mirrorCamera.layers.enable(1); // 使用图层1
4.2.2 物体维度切换逻辑
javascript复制// 可交互物体
const interactiveObjects = [];
function createInteractiveObject() {
const geometry = new THREE.SphereGeometry(0.3, 32, 32);
// 现实维度材质
const realityMaterial = new THREE.MeshStandardMaterial({
color: 0xdddddd,
metalness: 0.9,
roughness: 0.2,
envMap: environmentMap
});
// 镜像维度材质
const mirrorMaterial = new THREE.MeshPhongMaterial({
color: 0x44aaff,
transparent: true,
opacity: 0.7,
emissive: 0x0044aa,
emissiveIntensity: 0.5,
shininess: 100
});
const obj = {
mesh: new THREE.Mesh(geometry, realityMaterial),
isInMirror: false,
realityMaterial,
mirrorMaterial
};
// 随机位置
obj.mesh.position.set(
(Math.random() - 0.5) * 3,
1 + Math.random() * 2,
(Math.random() - 0.5) * 3
);
mainScene.add(obj.mesh);
interactiveObjects.push(obj);
// 添加物理特性
addPhysics(obj.mesh, !obj.isInMirror);
return obj;
}
// 维度切换函数
function toggleDimension(object) {
object.isInMirror = !object.isInMirror;
// 切换材质
object.mesh.material = object.isInMirror ?
object.mirrorMaterial :
object.realityMaterial;
// 切换物理特性
updatePhysics(object.mesh, !object.isInMirror);
// 切换场景
if (object.isInMirror) {
mainScene.remove(object.mesh);
mirrorScene.add(object.mesh);
} else {
mirrorScene.remove(object.mesh);
mainScene.add(object.mesh);
}
}
4.2.3 渲染循环与镜面效果
javascript复制function render() {
// 更新镜像相机位置
mirrorCamera.position.copy(mirror.position);
mirrorCamera.position.z += 5;
mirrorCamera.lookAt(mirror.position);
// 1. 渲染镜像场景到纹理
renderer.setRenderTarget(renderTarget);
renderer.render(mirrorScene, mirrorCamera);
renderer.setRenderTarget(null);
// 2. 将渲染结果应用到镜面
mirrorMaterial.map = renderTarget.texture;
mirrorMaterial.needsUpdate = true;
// 3. 渲染主场景
renderer.render(mainScene, camera);
// 4. 检查物体是否穿过镜面
checkMirrorIntersections();
requestAnimationFrame(render);
}
function checkMirrorIntersections() {
interactiveObjects.forEach(obj => {
// 计算物体到镜面的距离
const distance = mirror.position.distanceTo(obj.mesh.position);
const direction = new THREE.Vector3().subVectors(
obj.mesh.position,
mirror.position
).normalize();
// 检查是否在镜面后方
const dot = direction.dot(mirror.normal);
// 如果物体从前方穿过到后方,或者从后方穿过到前方
if ((dot < 0 && !obj.isInMirror) || (dot > 0 && obj.isInMirror)) {
toggleDimension(obj);
}
});
}
4.3 物理特性切换实现
4.3.1 物理引擎集成
javascript复制// 使用cannon.js物理引擎
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
// 添加物理体函数
function addPhysics(mesh, isSolid) {
const shape = new CANNON.Sphere(0.3);
const body = new CANNON.Body({
mass: isSolid ? 1 : 0, // 镜像维度中质量为0(不受重力)
shape,
position: new CANNON.Vec3(
mesh.position.x,
mesh.position.y,
mesh.position.z
),
material: isSolid ? solidMaterial : ghostMaterial
});
mesh.userData.physicsBody = body;
world.addBody(body);
}
// 更新物理特性
function updatePhysics(mesh, isSolid) {
const body = mesh.userData.physicsBody;
if (!body) return;
body.mass = isSolid ? 1 : 0;
body.material = isSolid ? solidMaterial : ghostMaterial;
// 重置速度和角速度
body.velocity.set(0, 0, 0);
body.angularVelocity.set(0, 0, 0);
// 如果从镜像维度回到现实维度,施加一个小的向上的力
if (isSolid) {
body.applyImpulse(
new CANNON.Vec3(0, 2, 0),
new CANNON.Vec3(0, 0, 0)
);
}
}
4.3.2 物理材质定义
javascript复制// 物理材质定义
const solidMaterial = new CANNON.Material("solid");
const ghostMaterial = new CANNON.Material("ghost");
const groundMaterial = new CANNON.Material("ground");
// 现实维度碰撞属性
const solidGroundContact = new CANNON.ContactMaterial(
solidMaterial,
groundMaterial,
{
friction: 0.4,
restitution: 0.3
}
);
// 镜像维度碰撞属性
const ghostGroundContact = new CANNON.ContactMaterial(
ghostMaterial,
groundMaterial,
{
friction: 0,
restitution: 0
}
);
world.addContactMaterial(solidGroundContact);
world.addContactMaterial(ghostGroundContact);
4.4 视觉效果增强
4.4.1 镜像维度特效
javascript复制// 添加镜像维度粒子效果
const mirrorParticles = new THREE.BufferGeometry();
const particleCount = 1000;
const positions = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 20;
positions[i3 + 1] = (Math.random() - 0.5) * 20;
positions[i3 + 2] = (Math.random() - 0.5) * 20;
sizes[i] = Math.random() * 0.2;
}
mirrorParticles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
mirrorParticles.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mirrorParticleMaterial = new THREE.PointsMaterial({
color: 0x4488ff,
size: 0.1,
transparent: true,
opacity: 0.5,
blending: THREE.AdditiveBlending
});
const mirrorParticleSystem = new THREE.Points(mirrorParticles, mirrorParticleMaterial);
mirrorScene.add(mirrorParticleSystem);
// 粒子动画
function animateMirrorParticles() {
const positions = mirrorParticles.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3 + 1] += 0.01;
if (positions[i3 + 1] > 10) {
positions[i3] = (Math.random() - 0.5) * 20;
positions[i3 + 1] = -10;
positions[i3 + 2] = (Math.random() - 0.5) * 20;
}
}
mirrorParticles.attributes.position.needsUpdate = true;
}
4.4.2 维度切换特效
javascript复制// 维度切换时的视觉效果
function playDimensionEffect(position) {
// 创建冲击波几何体
const geometry = new THREE.SphereGeometry(0.1, 32, 32);
const material = new THREE.MeshBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.8,
wireframe: true
});
const wave = new THREE.Mesh(geometry, material);
wave.position.copy(position);
mainScene.add(wave);
// 动画
let scale = 0.1;
function animate() {
scale += 0.2;
wave.scale.set(scale, scale, scale);
material.opacity = 1 - scale / 5;
if (scale < 5) {
requestAnimationFrame(animate);
} else {
mainScene.remove(wave);
}
}
animate();
}
性能优化建议:镜像空间效果对性能要求较高,可以考虑以下优化措施:
- 降低镜像场景的渲染分辨率
- 减少镜像场景中的多边形数量
- 使用LOD技术根据距离调整细节
- 限制同时活动的物理物体数量
- 对静态物体使用合并几何体技术
5. 3D空间绘画:体积笔触的实现技术
5.1 体积笔触的核心挑战
在3D空间中实现自然流畅的绘画效果需要解决几个关键问题:
- 笔触轨迹捕捉:准确记录用户在3D空间中的绘制路径
- 体积表现:将路径转换为有体积感的3D几何体
- 物理模拟:使笔触具有重量感和自然下垂效果
- 交互编辑:支持对已有笔触的修改和删除
传统2D绘画的算法无法直接应用于3D空间,我们需要结合曲线生成、几何体构建和物理模拟等技术。
5.2 技术实现方案
5.2.1 基础绘画系统
javascript复制// 绘画系统状态
const drawingState = {
isDrawing: false,
currentStroke: null,
strokes: [],
points: []
};
// 鼠标/触摸事件处理
function startDrawing(event) {
drawingState.isDrawing = true;
drawingState.points = [];
// 获取起始点
const point = get3DPointerPosition(event);
if (point) {
drawingState.points.push(point);
}
}
function continueDrawing(event) {
if (!drawingState.isDrawing) return;
const point = get3DPointerPosition(event);
if (point && drawingState.points.length > 0) {
// 检查与上一点的距离,避免点太密集
const lastPoint = drawingState.points[drawingState.points.length - 1];
if (point.distanceTo(lastPoint) > 0.1) {
drawingState.points.push(point);
updateCurrentStroke();
}
}
}
function endDrawing() {
if (drawingState.isDrawing && drawingState.points.length > 2) {
// 完成当前笔触
if (drawingState.currentStroke) {
drawingState.strokes.push(drawing