此功能借助meshline插件來實現最爲簡便,初學就暫不探究其原理了。
github地址: https://github.com/spite/THREE.MeshLine
本文主要學習了躍焱邵隼作者的源代碼,但由於作者已將自己的插件一步一步壯大,導致初學時,無法準確切入到核心,因此特別記錄一下。
https://www.wellyyss.cn/ysThree/main/app.html
效果
先看最基本的
function initThree(el, options) {
options = options || {}
const t = this
appInstance = this
const width = el.offsetWidth
const height = el.offsetHeight
const asp = width / height
// scene
const scene = new THREE.Scene()
// camera
let camera
if (options.camera) {
camera = options.camera
} else {
camera = new THREE.PerspectiveCamera(45, asp, 1, 100000)
window.addEventListener('resize', function() {
camera.aspect = el.offsetWidth / el.offsetHeight
renderer.setSize(el.offsetWidth, el.offsetHeight) // 重新獲取
camera.updateProjectionMatrix()
renderer.render(scene, camera)
}, false)
}
camera.position.set(30, 30, 30)
// renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
el.append(renderer.domElement)
renderer.setClearColor(options.clearColor || '#000')
// 輔助
if (options.axes) scene.add(new THREE.AxesHelper(10))// 座標軸輔助紅x 綠y 藍z
if (options.gridHelper) scene.add(new THREE.GridHelper(100, 100))// 網格參考線
//按序渲染
renderer.sortObjects = options.sortObjects
// to the instance
t.renderer = renderer
t.scene = scene
t.camera = camera
t.el = el
}
const el = document.getElementById('box')
const app = new initThree(el,{
// gridHelper:true,//網格參考線
// axes:true,//座標輔助
clearColor:'#000'//畫布顏色
})
const camera = app.camera
const renderer = app.renderer
const scene = app.scene
function initControls(scene,camera,renderer) {
const controls = new THREE.OrbitControls( camera, renderer.domElement );
// 如果使用animate方法時,將此函數刪除
controls.addEventListener( 'change', ()=>{
renderer.render( scene, camera );
});
// // 使動畫循環使用時阻尼或自轉 意思是否有慣性
// controls.enableDamping = true;
// //動態阻尼係數 就是鼠標拖拽旋轉靈敏度
// //controls.dampingFactor = 0.25;
// //是否可以縮放
// controls.enableZoom = true;
// //是否自動旋轉
// controls.autoRotate = true;
// controls.autoRotateSpeed = 0.5;
// //設置相機距離原點的最遠距離
// controls.minDistance = 1;
// //設置相機距離原點的最遠距離
// controls.maxDistance = 200;
// //是否開啓右鍵拖拽
//controls.enablePan = true;
return controls
}
var controls = initControls(scene,camera,renderer)
const clock = new THREE.Clock()
//add light
const directionalLight = new THREE.DirectionalLight( '#fff' )
directionalLight.position.set( 30, 30, 30 ).normalize()
scene.add( directionalLight )
const ambientLight = new THREE.AmbientLight('#fff',0.3) // obj 唯一 id
scene.add(ambientLight)
const pointList1 = [
[20,5,10],
[10,5,-9],
[10,5,20],
[-40,5,-40]
]
let line1
textureLoader.load( '../../images/ysThree/green_line.png', function (texture1) {
const material1 = new MeshLineMaterial({
color: "green",
map: texture1,
useMap: true,
lineWidth: 4,
resolution: resolution,
dashArray: 0.8, // 破折號之間的長度和間距。(0 -無破折號)
dashRatio: 0.5, // 定義可見和不可見之間的比率(0 -更可見,1 -更不可見)。
dashOffset: 0,
transparent: true,
sizeAttenuation: 1, //使線寬不變,不管距離(1個單位是屏幕上的1px)(0 -衰減,1 -不衰減)
side: THREE.FrontSide,
depthTest: true,
blending: THREE.AdditiveBlending,
near: camera.near,
far: camera.far,
})
const l = []
pointList1.forEach(e => l.push(new THREE.Vector3(e[0], e[1], e[2])))
let curve = new THREE.CatmullRomCurve3(l) // 曲線路徑
const geo = new THREE.Geometry()
geo.vertices = curve.getPoints( 50)
console.log(geo)
const meshLine = new MeshLine()
meshLine.setGeometry(geo)
console.log(meshLine.geometry)
line1=new THREE.Mesh(meshLine.geometry, material1)
scene.add(line1)
})
function render() {
controls.update(clock.getDelta())
renderer.render( scene,camera)
requestAnimationFrame(render)
//
if(line1){
line1.material.uniforms.dashOffset.value -= 0.01
}
}
render()
由上文可以看到,其實核心就是基於meshline作者提供的插件來完成的。
完成代碼如下:
<script src="../../plugins/threeLibrary/three.min.js"></script>
<script src="../../plugins/threeLibrary/js/controls/OrbitControls.js"></script>
<script src="../../plugins/threeLibrary/js/lines/MeshLine.js"></script>
<script>
function initThree(el, options) {
options = options || {}
const t = this
appInstance = this
const width = el.offsetWidth
const height = el.offsetHeight
const asp = width / height
// scene
const scene = new THREE.Scene()
// camera
let camera
if (options.camera) {
camera = options.camera
} else {
camera = new THREE.PerspectiveCamera(45, asp, 1, 100000)
window.addEventListener('resize', function() {
camera.aspect = el.offsetWidth / el.offsetHeight
renderer.setSize(el.offsetWidth, el.offsetHeight) // 重新獲取
camera.updateProjectionMatrix()
renderer.render(scene, camera)
}, false)
}
camera.position.set(30, 30, 30)
// renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
el.append(renderer.domElement)
renderer.setClearColor(options.clearColor || '#000')
// 輔助
if (options.axes) scene.add(new THREE.AxesHelper(10))// 座標軸輔助紅x 綠y 藍z
if (options.gridHelper) scene.add(new THREE.GridHelper(100, 100))// 網格參考線
//按序渲染
renderer.sortObjects = options.sortObjects
// to the instance
t.renderer = renderer
t.scene = scene
t.camera = camera
t.el = el
}
const el = document.getElementById('box')
const app = new initThree(el,{
// gridHelper:true,//網格參考線
// axes:true,//座標輔助
clearColor:'#000'//畫布顏色
})
const camera = app.camera
const renderer = app.renderer
const scene = app.scene
function initControls(scene,camera,renderer) {
const controls = new THREE.OrbitControls( camera, renderer.domElement );
// 如果使用animate方法時,將此函數刪除
controls.addEventListener( 'change', ()=>{
renderer.render( scene, camera );
});
// // 使動畫循環使用時阻尼或自轉 意思是否有慣性
// controls.enableDamping = true;
// //動態阻尼係數 就是鼠標拖拽旋轉靈敏度
// //controls.dampingFactor = 0.25;
// //是否可以縮放
// controls.enableZoom = true;
// //是否自動旋轉
// controls.autoRotate = true;
// controls.autoRotateSpeed = 0.5;
// //設置相機距離原點的最遠距離
// controls.minDistance = 1;
// //設置相機距離原點的最遠距離
// controls.maxDistance = 200;
// //是否開啓右鍵拖拽
//controls.enablePan = true;
return controls
}
var controls = initControls(scene,camera,renderer)
const clock = new THREE.Clock()
//add light
const directionalLight = new THREE.DirectionalLight( '#fff' )
directionalLight.position.set( 30, 30, 30 ).normalize()
scene.add( directionalLight )
const ambientLight = new THREE.AmbientLight('#fff',0.3) // obj 唯一 id
scene.add(ambientLight)
camera.position.set(100,100,100)
const resolution = new THREE.Vector2( el.offsetWidth, el.offsetHeight );
const textureLoader = new THREE.TextureLoader()
function getSphereHeightPoints (v0, v3, n1, n2, p0) {
// 夾角
const angle = (v0.angleTo(v3) * 180) / Math.PI / 10 // 0 ~ Math.PI
const aLen = angle * (n1 || 10)
const hLen = angle * angle * (n2 || 120)
p0 = p0 || new THREE.Vector3(0, 0, 0) // 默認以 座標原點爲參考對象
// 法線向量
const rayLine = new THREE.Ray(p0, v0.clone().add(v3.clone()).divideScalar(2))
// 頂點座標
const vtop = rayLine.at(hLen / rayLine.at(1).distanceTo(p0))
// 計算制高點
const getLenVector = (v1, v2, len) => v1.lerp(v2, len / v1.distanceTo(v2))
// 控制點座標
return [getLenVector(v0.clone(), vtop, aLen), getLenVector(v3.clone(), vtop, aLen)]
}
/* **** **** **** ****/
function createAnimateLine (option) {
let curve
if (option.kind === 'sphere') { // 由兩點之間連線成貝塞爾曲線
const sphereHeightPointsArgs = option.sphereHeightPointsArgs
const pointList = this.getSphereHeightPoints(...sphereHeightPointsArgs) // v0,v3,n1,n2,p0
curve = new THREE.CubicBezierCurve3(sphereHeightPointsArgs[0], pointList[0], pointList[1], sphereHeightPointsArgs[1])
} else { // 由多個點數組構成的曲線 通常用於道路
const l = []
option.pointList.forEach(e => l.push(new THREE.Vector3(e[0], e[1], e[2])))
curve = new THREE.CatmullRomCurve3(l) // 曲線路徑
}
if (option.type === 'pipe') { // 使用管道線
// 管道體
const tubeGeometry = new THREE.TubeGeometry(curve, option.number || 50, option.radius || 1, option.radialSegments)
return new THREE.Mesh(tubeGeometry, option.material)
} else { // 使用 meshLine
if (!MeshLine || !MeshLineMaterial) console.error('you need import MeshLine & MeshLineMaterial!')
else {
const geo = new THREE.Geometry()
geo.vertices = curve.getPoints(option.number || 50)
const meshLine = new MeshLine()
meshLine.setGeometry(geo)
return new THREE.Mesh(meshLine.geometry, option.material)
}
}
}
const pointList1 = [
[20,5,10],
[10,5,-9],
[10,5,20],
[-40,5,-40]
]
let line1
textureLoader.load( '../../images/ysThree/green_line.png', function (texture1) {
const material1 = new MeshLineMaterial({
color: "green",
map: texture1,
useMap: true,
lineWidth: 4,
resolution: resolution,
dashArray: 0.8, // 破折號之間的長度和間距。(0 -無破折號)
dashRatio: 0.5, // 定義可見和不可見之間的比率(0 -更可見,1 -更不可見)。
dashOffset: 0,
transparent: true,
sizeAttenuation: 1, //使線寬不變,不管距離(1個單位是屏幕上的1px)(0 -衰減,1 -不衰減)
side: THREE.FrontSide,
depthTest: true,
blending: THREE.AdditiveBlending,
near: camera.near,
far: camera.far,
})
const l = []
pointList1.forEach(e => l.push(new THREE.Vector3(e[0], e[1], e[2])))
let curve = new THREE.CatmullRomCurve3(l) // 曲線路徑
const geo = new THREE.Geometry()
geo.vertices = curve.getPoints( 50)
console.log(geo)
const meshLine = new MeshLine()
meshLine.setGeometry(geo)
console.log(meshLine.geometry)
line1=new THREE.Mesh(meshLine.geometry, material1)
scene.add(line1)
})
/** 2:繪製普通pipeLine**/
const pointList2 = [
[-20,5,-10],
[30,5,-15],
[10,5,20],
[40,5,40]
]
const texture2 = textureLoader.load("../../images/ysThree/red_line.png")
texture2.wrapS = texture2.wrapT = THREE.RepeatWrapping; //每個都重複
texture2.repeat.set(1, 1)
const material2 = new THREE.MeshBasicMaterial({map:texture2,side:THREE.BackSide,transparent:true})
texture2.needsUpdate = true
const line2 = createAnimateLine({
// kind: 'sphere',//默認不填 爲普通 ; 如爲sphere,則表示球面建點
type: 'pipe',//默認不填 爲MeshLine ; 如爲pipe,則表示管道線
pointList: pointList2,
material: material2,
number: 100
})
scene.add(line2)
/** 1:在球面上繪製meshLine**/
const v0 = new THREE.Vector3( -80, 10, 0 )
const v3 = new THREE.Vector3( 80, 10, 0 )
let line3
textureLoader.load( '../../images/ysThree/green_line.png', function (texture3) {
const material3 = new MeshLineMaterial({
color: "green",
map: texture3,
useMap: true,
lineWidth: 4,
resolution: resolution,
dashArray: 0.8, // 破折號之間的長度和間距。(0 -無破折號)
dashRatio: 0.5, // 定義可見和不可見之間的比率(0 -更可見,1 -更不可見)。
dashOffset: 0,
transparent: true,
sizeAttenuation: 1, //使線寬不變,不管距離(1個單位是屏幕上的1px)(0 -衰減,1 -不衰減)
side: THREE.FrontSide,
depthTest: true,
blending: THREE.AdditiveBlending,
near: camera.near,
far: camera.far,
})
line3 = createAnimateLine({
kind: 'sphere',//默認不填 爲普通 ; 如爲sphere,則表示球面建點
// type: 'pipe',//默認不填 爲MeshLine ; 如爲pipe,則表示管道線
sphereHeightPointsArgs: [v0,v3],
material: material3
})
scene.add(line3)
})
/** 1:在球面上繪製pipeLine**/
const v0_1 = new THREE.Vector3( -60, 10, 0 )
const v3_1 = new THREE.Vector3( 60, 10, 0 )
const texture4 = textureLoader.load("../../images/ysThree/red_line.png")
texture4.wrapS = texture4.wrapT = THREE.RepeatWrapping; //每個都重複
texture4.repeat.set(1, 1)
const materia4 = new THREE.MeshBasicMaterial({map:texture4,side:THREE.BackSide,transparent:true})
texture4.needsUpdate = true
const line4 = createAnimateLine({
kind: 'sphere',//默認不填 爲普通 ; 如爲sphere,則表示球面建點
type: 'pipe',//默認不填 爲MeshLine ; 如爲pipe,則表示管道線
sphereHeightPointsArgs: [v0_1,v3_1],
material: materia4,
number: 100,
radius: 1 // 默認
})
scene.add(line4)
/* **** **** **** ****/
function render() {
controls.update(clock.getDelta())
renderer.render( scene,camera)
requestAnimationFrame(render)
//
if(line1){
line1.material.uniforms.dashOffset.value -= 0.01
}
//
texture2.offset.x -= 0.01
//
if(line3){
line3.material.uniforms.dashOffset.value -= 0.01
}
texture4.offset.x -= 0.01
}
render()
</script>