Three.js 分子/晶体结构解析教程
环境准备
目前使用的Three.js版本是0.165.0
npm i three
最终效果
结构化学式Al4As14
思路解析
绘制分子/晶体结构首先将这结构拆解来看,目前显示的效果整个结构三个部分
1、晶格 - 白色线框
2、原子 - 圆形
3、建 - 两个圆锥体
1、基本配置
首先需要配置基本场景、相机、渲染器
import * as THREE from 'three'
// 场景
this.scene = new THREE.Scene();
// 正交相机
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 10000000);
this.camera.position.set(0, 0, 5.5);
// 渲染器
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(width, height);
this.renderer.setClearColor('#383838', 1);
dom.appendChild(this.renderer.domElement);
//配置环境光
this.ambientLight(this.scene)
// 开始渲染
const animate = () => {
requestAnimationFrame(animate);
this.controls.update();
// 渲染主场景
this.renderer.clear();
this.renderer.render(this.scene, this.camera);
// 渲染坐标轴场景
this.renderer.autoClear = false; // 不清除颜色缓存
this.renderer.clearDepth(); // 清除深度缓存
// 设置坐标轴的渲染区域
this.renderer.setViewport(0, 0, width / 4, height / 4);
this.renderer.render(this.Helper.axesScene, this.Helper.axesCamera);
// 复原视口
this.renderer.setViewport(0, 0, width, height);
this.renderer.autoClear = true; // 恢复 autoClear
}
animate()
环境光函数,在这里采用了两种光
ambientLight(scene){
// 环境光
let AmbientLight = new THREE.AmbientLight('#fff',2)
scene.add(AmbientLight)
// 创建平行光
const DirectionalLight = new THREE.DirectionalLight(0xffffff, 1); // 颜色为白色,强度为 1
// 设置光源位置
DirectionalLight.position.set(1, 1, 1).normalize(); // 光源位置为 (1, 1, 1),并进行标准化处理
// 将光源添加到场景中
scene.add(DirectionalLight);
}
2、解析结构
在这里需要准备一个json文件,需要原子的坐标,晶格矢量,建索引
lattice - 标识晶格矢量
atom - 原子信息 l 原子名称全大写,xyz 坐标信息
band - 表示需要连健原子索引如果[2,4],表示下表2和下表4的原子进行连健
{
"lattice": [
[5.6,0,0],
[0,5.6,0],
[0,0,5.6],
[0,0,0]
],
"atom": [
{
"l": "AL",
"x": 0,
"y": 0,
"z": 0,
"id": 0
},
{
"l": "AL",
"x": 0,
"y": 2.8,
"z": 2.8,
"id": 1
},
{
"l": "AL",
"x": 2.8,
"y": 0,
"z": 2.8,
"id": 2
},
{
"l": "AL",
"x": 2.8,
"y": 2.8,
"z": 0,
"id": 3
},
{
"l": "AS",
"x": 0.25,
"y": 0.25,
"z": 0.25,
"id": 4
},
],
"band": [
[2,4]
]
}
3、进行解析
引入准备好的json文件
import data from 'data.json'
讲个绘制的过程分成了3步骤
1、原子的绘制
2、连健
3、晶格绘制
1)原子部分
// 原子
drawAtom(atoms){
const atomGroup = new THREE.Group();
atomGroup.name = 'atom'
const ballConstruction = (color,name) => {
let radius = 0.3; // 球体的半径
let widthSegments = 32; // 水平方向的分段数
let heightSegments = 16; // 垂直方向的分段数
let geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
let newColor = new THREE.Color();
newColor.setStyle(color);
let material = new THREE.MeshStandardMaterial ({
color: newColor, // 设置球体颜色
roughness: 0, // 设置粗糙度
metalness: 0, // 设置金属度
});
let sphere = new THREE.Mesh(geometry, material);
sphere.name = name
return sphere
}
atoms.forEach(atom => {
const initAtom = ballConstruction(atomColor[atom.l.toLowerCase()],atom.l)
initAtom.position.set(atom.x,atom.y,atom.z)
atomGroup.add(initAtom);
})
return atomGroup
}
2)连健部分
连健需要的是2个原子的索引,因为两个原子的种类可能不同需要绘制两个圆柱。
// 连键
drawBand(atom,band){
const bandGroup = new THREE.Group();
bandGroup.name = 'band'
const bandConstruction = (point1,point2,color) => {
// 计算两个点之间的距离
const distance = point1.distanceTo(point2);
// 计算圆柱体的高度和位置
const height = distance; // 圆柱体的高度为两点之间的距离
const position = point1.clone().add(point2).divideScalar(2); // 圆柱体的位置为两点之间的中点
// 计算圆柱体的方向向量
const direction = point2.clone().sub(point1).normalize();
// 创建圆柱体的参数
const radiusTop = 0.1; // 顶部半径
const radiusBottom = 0.1; // 底部半径
const radialSegments = 32; // 周向分段数
const heightSegments = 1; // 高度分段数
const openEnded = false; // 是否封闭
// 创建圆柱体的几何体
const geometry = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded);
// 创建圆柱体的材质
let newColor = new THREE.Color();
newColor.setStyle(color);
const material = new THREE.MeshStandardMaterial({
color: newColor, // 设置球体颜色
roughness: 0, // 设置粗糙度
metalness: 0, // 设置金属度
});
// 创建圆柱体的网格对象
const cylinder = new THREE.Mesh(geometry, material);
// 设置圆柱体的位置和方向
cylinder.position.copy(position);
cylinder.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
return cylinder
}
band.forEach(keys => {
let start = atom[keys[0]]
let end = atom[keys[1]]
// 两个原子的坐标
const point1 = new THREE.Vector3(start.x, start.y, start.z); // 替换 x1、y1 和 z1 为第一个点的坐标
const point2 = new THREE.Vector3(end.x, end.y, end.z); // 替换 x2、y2 和 z2 为第二个点的坐标
// 计算两个原子的中间点坐标
const midpoint = new THREE.Vector3().addVectors(point1, point2).multiplyScalar(0.5);
// 需要添加两个圆柱
// 原子1到中间
const band1 = bandConstruction(point1,midpoint,atomColor[atom[keys[0]].l.toLowerCase()])
band1.name = `band_left_${atom[keys[0]].l}_${atom[keys[0]].id}`
// 原子2到中间
const band2 = bandConstruction(midpoint,point2,atomColor[atom[keys[1]].l.toLowerCase()])
band1.name = `band_right_${atom[keys[1]].l}_${atom[keys[1]].id}`
bandGroup.add(band1)
bandGroup.add(band2)
console.log(midpoint);
})
return bandGroup
}
3)晶格部分
晶格需要根据json文件的适量去计算所有顶点的坐标
// 晶格
drawLattice(vectors) {
const atomGroup = new THREE.Group();
atomGroup.name = 'lattice'
const [a1, a2, a3, origin] = vectors;
const points = [];
// 遍历所有可能的 n1, n2, n3 组合
for (let n1 = 0; n1 <= 1; n1++) {
for (let n2 = 0; n2 <= 1; n2++) {
for (let n3 = 0; n3 <= 1; n3++) {
const x = n1 * a1[0] + n2 * a2[0] + n3 * a3[0];
const y = n1 * a1[1] + n2 * a2[1] + n3 * a3[1];
const z = n1 * a1[2] + n2 * a2[2] + n3 * a3[2];
points.push(...[x, y, z]);
}
}
}
// 计算后的八个顶点
const vertices = new Float32Array(points);
// 计算后顶点的链接关系
const edges = new Uint16Array([
0, 1,
0, 2,
0, 4,
1, 3,
1, 5,
2, 3,
2, 6,
3, 7,
4, 5,
4, 6,
5, 7,
6, 7
]);
// 创建几何体并添加顶点和边
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(edges, 1));
// 创建线条材质
const material = new THREE.LineBasicMaterial({ color: 0xffffff });
// 创建线条
const lineSegments = new THREE.LineSegments(geometry, material);
atomGroup.add(lineSegments)
const OlineText = (toArr, text, color) => {
const label = this.createTextLabel(text, color)
label.position.set(...toArr);
atomGroup.add(label);
}
// 画OA,OB,OC
const Oline = (toArr,color,text) => {
// 定义线条的几何体
const points = [];
points.push(new THREE.Vector3( 0,0, 0));
points.push(new THREE.Vector3(toArr[0],toArr[1],toArr[2]));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// 定义线条的材质
const material = new THREE.LineBasicMaterial({ color });
// 创建线条并添加到场景中
const line = new THREE.Line(geometry, material);
atomGroup.add(line);
OlineText([toArr[0] - 0.2, toArr[1] - 0.2, toArr[2] - 0.2], text, color)
}
// OA
Oline(vectors[0],'red','a')
// OB
Oline(vectors[1],'green','b')
// OC
Oline(vectors[2],'blue','c')
// O
OlineText([-0.2,-0.2,-0.2],'o','#fff')
// 将最后绘制完成的内容进行返回
return atomGroup
}
// 坐标轴的文字
createTextLabel (labelText, color,size = 1.5) {
const canvas = document.createElement('canvas');
canvas.width = 200; // 调整 canvas 大小以容纳文字
canvas.height = 200;
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
context.font = '120px Times New Roman'; // 增大字体尺寸
context.fillStyle = color;
context.fillText(labelText, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas); // 使用 CanvasTexture
texture.premultiplyAlpha = false;
texture.needsUpdate = true;
const material = new THREE.SpriteMaterial({ map: texture ,alphaTest: 0.2 , transparent: true});
const sprite = new THREE.Sprite(material);
sprite.scale.set(size, size, size); // 调整标签尺寸
return sprite;
}
4)绘制位置调整
因为绘制到画布如果不进行调整那么他的效果是这样的,并不在正中心,那么需要进行调整
// 将结构调整到中间位置
setMolCenter(content){
const {maxX,maxY,maxZ} = this.getMinAndMAxPosition()
content.position.set(
parseFloat(-Math.abs(maxX)).toFixed(2) / 2,
parseFloat(-Math.abs(maxY)).toFixed(2) / 2,
parseFloat(-Math.abs(maxZ )).toFixed(2) / 2
)
}
// 获取内容的最大和最小坐标
getMinAndMAxPosition(){
// 初始化一个空的 Box3
const boundingBox = new THREE.Box3();
// 遍历 atomGroup 中的所有子对象并更新 boundingBox
this.atomGroup.children.forEach(child => {
const childBoundingBox = new THREE.Box3().setFromObject(child);
boundingBox.union(childBoundingBox);
});
const maxX = boundingBox.max.x;
const maxY = boundingBox.max.y;
const maxZ = boundingBox.max.z;
const minX = boundingBox.min.x;
const minY = boundingBox.min.y;
const minZ = boundingBox.min.z;
return {
maxX,maxY,maxZ,
minX,minY,minZ
}
}
5)绘制
调用上述代码,将所有的内容放到atomGroup 中,将atomGroup 添加到场景即可实现完整逻辑
const {lattice, atom, band} = data
this.atomGroup = new THREE.Group();
this.atomGroup.name = 'structure'
// 原子绘制
if (atom && atom.length){
const initAtom = this.drawAtom(atom)
this.atomGroup.add(initAtom)
}
// 连键
if (band && band.length){
const initBand = this.drawBand(atom,band)
this.atomGroup.add(initBand)
}
// 晶格绘制
if (lattice && lattice.length){
const initLattice = this.drawLattice(lattice)
this.atomGroup.add(initLattice)
}
// 讲内容调整到画布中间位置
this.setMolCenter(this.atomGroup)
标签:教程,const,atomGroup,THREE,Three,let,atom,new,js
From: https://blog.csdn.net/chenyong_1997/article/details/140245082