【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)
  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值