2023-03-14 13:56

[OpenCV] 以最亮顏色的色相來進行灰階處理

OpenCV-Python 教學 https://docs.opencv.org/3.4.16/d6/d00/tutorial_py_root.html

原始圖片

原圖因為燈光散射的關係,整個圖片都出現發紅的現象


預設灰階 cv2.imread('01.jpg', cv2.IMREAD_GRAYSCALE)

有部分的色差變得不明顯,之後要進行辨識變得很困難


色相比例的灰階

透過色相的方式就好很多


HSV 的數值定義

  • H: 色相 0~360度
  • S: 飽和度 0~1
  • V: 明度 (黑)0~1(白)

OpenCV HSV 的數值定義

  • H: 0 ~ 180 => H / 2
  • S: 0 ~ 255 => S * 255
  • V: 0 ~ 255 => V * 255

相依套件:

  1. pip install opencv-python 
  2. pip install py-linq     # https://viralogic.github.io/py-enumerable/ 

程式:

  1. from py_linq import Enumerable 
  2. import cv2 
  3. import numpy as np 
  4.  
  5.  
  6. def get_hue(img): 
  7.    ''' 圖片色相 ''' 
  8.  
  9.    img = cv2.resize(img, (32, 32), interpolation = cv2.INTER_LINEAR) 
  10.    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) 
  11.    pixel_hsv = np.reshape(img_hsv,(-1,3)) 
  12.  
  13.    result = Enumerable(pixel_hsv)\ 
  14.        .order_by_descending(lambda p: p[2])\ 
  15.        .then_by_descending(lambda p: p[1])\ 
  16.        .first() 
  17.  
  18.    print(f'[result]: {result}') 
  19.    return result[0] 
  20.  
  21.  
  22.  
  23. def get_gray_rate(hue): 
  24.    ''' 根據色相計算灰階比例 ''' 
  25.  
  26.    hue_full = np.uint8([[[hue,255,255]]]) 
  27.    hue_bgr = cv2.cvtColor(hue_full, cv2.COLOR_HSV2BGR) 
  28.    (hue_b, hue_g, hue_r) = hue_bgr[0,0] 
  29.  
  30.    hue_total =  0.0 + hue_b + hue_g + hue_r 
  31.  
  32.    return [ 
  33.        hue_b / hue_total, 
  34.        hue_g / hue_total, 
  35.        hue_r / hue_total, 
  36.    ] 
  37.  
  38.  
  39.  
  40. img = cv2.imread('01.jpg') 
  41.  
  42. hue = get_hue(img) 
  43. gray_rate = get_gray_rate(hue) # 灰階比例 
  44.  
  45. print(f'hue:{hue}, gray_rate: {gray_rate}') 
  46.  
  47. # 灰階轉換 
  48. img_gray = cv2.transform(img, np.array([gray_rate])) 
  49.  
  50. cv2.imshow("img", img) 
  51. cv2.imshow("img_gray", img_gray) 
  52. cv2.waitKey(0) 
  53. cv2.destroyAllWindows() 
2023-03-14 13:23

[ThreeJS] 噴水粒子

要完成這個效果有三個問題要解決

  1. 自然的粒子分布
  2. 粒子的拋物線移動
  3. 粒子的擴散效果

我採用簡單的方式達成

  1. 用亂數產生分段的粒子團
  2. 用二次貝茲曲線建立拋物線
  3. 用粒子團的放大產生擴散效果

因為我用簡單的亂數分布粒子,粒子團會有立方體現象,球體分布會更好,但多個區段連起來就不明顯了。

二次貝茲曲線最大的困難在計算出結尾座標的位置,透過轉換到世界空間去定位出平面高度的座標,再轉換回物體空間來計算出結尾座標。

動畫用 AnimationMixer 去控制,只要定出每段粒子團的開始播放時間,粒子團就會接續的移動。

circle.png

程式:

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4. renderer.setPixelRatio(window.devicePixelRatio); 
  5. renderer.setSize(window.innerWidth, window.innerHeight); 
  6.  
  7. /* 初始化場景 */ 
  8. const scene = new THREE.Scene(); 
  9. scene.background = new THREE.Color('#111'); /* 背景顏色 */ 
  10. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  11.  
  12. /* 地面 */ 
  13. const floorMesh = new THREE.Mesh( 
  14.    new THREE.PlaneGeometry(200, 200), 
  15.    new THREE.MeshBasicMaterial({ color: '#DDD', depthWrite: false }) 
  16. ); 
  17. floorMesh.rotation.x = THREE.MathUtils.degToRad(-90); 
  18. scene.add(floorMesh); 
  19.  
  20.  
  21. /* 初始化鏡頭 */ 
  22. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  23. camera.position.set(0, 10, 22); 
  24.  
  25. /* 初始化軌道控制,鏡頭的移動 */ 
  26. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  27. orbitControls.update(); 
  28.  
  29.  
  30. /* 動畫刷新器 */ 
  31. const mixers = []; 
  32.  
  33. function newMixer(target) { 
  34.    const mixer = new THREE.AnimationMixer(target); 
  35.    mixers.push(mixer); 
  36.    return mixer; 
  37. } 
  38.  
  39. /* 動畫計時器 */ 
  40. const clock = new THREE.Clock(); 
  41.  
  42. /* 渲染週期 */ 
  43. function renderCycle() { 
  44.    const delta = clock.getDelta(); 
  45.    mixers.forEach(x => x.update(delta)); 
  46.  
  47.    renderer.render(scene, camera); 
  48.    requestAnimationFrame(renderCycle); 
  49. } 
  50. renderCycle(); 
  51.  
  52.  
  53.  
  54. /*---------------------------------------------------------------*/ 
  55.  
  56. /* 水管物件,水滴粒子群組會附加在這裡 */ 
  57. const pipeMesh = new THREE.Mesh( 
  58.    new THREE.CylinderGeometry(0.5, 0.5, 9, 16), 
  59.    new THREE.MeshBasicMaterial( {color: '#eea236'} ) 
  60. ); 
  61. pipeMesh.rotation.x = THREE.MathUtils.degToRad(0); 
  62. pipeMesh.position.set(0, 4, 0); 
  63. scene.add(pipeMesh); 
  64.  
  65.  
  66. const dripGroup = new THREE.Group(); 
  67. dripGroup.position.set(0, 4, 0); 
  68. pipeMesh.add(dripGroup); 
  69.  
  70.  
  71. /* 水滴材質 */ 
  72. const dripMaterial = new THREE.PointsMaterial({ 
  73.    map: new THREE.TextureLoader().load('circle.png'), 
  74.    color: '#0aa', 
  75.    size: 1, 
  76.    opacity: 0.7, 
  77.    depthWrite: false, 
  78.    transparent: true, 
  79. }); 
  80.  
  81.  
  82. /* 建立水滴,用亂數建立粒子點 */ 
  83. for (let i = 0; i < 60; i++) { 
  84.    const vertices = []; 
  85.  
  86.    for (let j = 0; j < 40; j++) { 
  87.        const x = Math.random() - 0.5; 
  88.        const y = Math.random() - 0.5; 
  89.        const z = Math.random() - 0.5; 
  90.        vertices.push(x, y, z); 
  91.    } 
  92.  
  93.    const geometry = new THREE.BufferGeometry(); 
  94.    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); 
  95.  
  96.    const particles = new THREE.Points(geometry, dripMaterial); 
  97.    dripGroup.add(particles); 
  98.  
  99.    newMixer(particles); /* 水滴動畫 */ 
  100. } 
  101.  
  102.  
  103. /*---------------------------------------------------------------*/ 
  104.  
  105. const cycle = 2; /* 循環週期,會影響水流速度 */ 
  106. const scale = 8; /* 擴散大小 */ 
  107. const length = 14; /* 水流長度 */ 
  108.  
  109.  
  110. /* 二次貝茲曲線,用來取得拋物線的座標點 */ 
  111. const curve = new THREE.QuadraticBezierCurve3(); 
  112. curve.v1.set(0, length, 0); /* 曲線控制點座標 */ 
  113.  
  114.  
  115. /* 計算結尾座標 */ 
  116. const rootPos = dripGroup.getWorldPosition(new THREE.Vector3()); 
  117. const quaternion = dripGroup.getWorldQuaternion(new THREE.Quaternion()); 
  118. const toPos = curve.v1.clone(); 
  119. toPos.applyQuaternion(quaternion); /* 轉換到世界空間 */ 
  120.  
  121. /* 當水流是向上時,增加平面位置 */ 
  122. if (toPos.y > (length / 3)) { 
  123.    toPos.x *= 1.8; 
  124.    toPos.z *= 1.8; 
  125. } 
  126. toPos.y = Math.min(-rootPos.y, toPos.y * 1.5); /* 將結尾拉回平面高度 */ 
  127. toPos.applyQuaternion(quaternion.conjugate()); /* 轉換回物體空間 */ 
  128. curve.v2.copy(toPos); /* 曲線結尾點座標 */ 
  129.  
  130.  
  131. /* 建立拋物線及擴散動畫 */ 
  132. const points = curve.getPoints(10); /* 曲线分段 */ 
  133.  
  134. const curveTime = []; 
  135. const curvePos = []; 
  136. points.forEach((v, i) => { 
  137.    curveTime.push(cycle * i / points.length); 
  138.    curvePos.push(v.x, v.y, v.z); 
  139. }); 
  140. const posTrack = new THREE.VectorKeyframeTrack('.position', curveTime, curvePos); 
  141. const scaleTrack = new THREE.VectorKeyframeTrack('.scale', [0, cycle], [0, 0, 0, scale, scale, scale]); 
  142. const clip = new THREE.AnimationClip('scale', cycle, [posTrack, scaleTrack]); 
  143.  
  144. mixers.forEach((mixer, i) => { 
  145.    const action = mixer.clipAction(clip); 
  146.    action.time = cycle * i / mixers.length; 
  147.    action.play(); 
  148. }); 
2023-03-10 14:33

TypeScript 強轉型的地雷

用 Visual Studio 撰寫 TypeScript 一整個就很開心開心,只要好好的進行型態宣告,就可享受到跟強型別一樣的 IntelliSense 提示,實在太開心了,然後就大意了,忘記了其實骨子裡還是 JavaScript 這件事。

在取回後端的 json 資料的時候,貪圖方便就直接進行強轉型,哈哈!這裡存在不確定性,我就踩到地雷了!當然如果後端是可靠的,直接強轉型是不會有問題的。

我想都已經進行型別宣告了,難到不能自動轉型嗎?哈哈!在網路上找了很久都沒找到,找到的方式都需要二次宣定去做轉型。

簡單的展示一下這個問題,建立一個 any 的物件 a,然後強轉型成 DataModel 的物件 b,這時候 a 跟 b 其實還是同一個 Instance

  1. interface DataModel { 
  2.    id: number; 
  3.    status: string; 
  4. } 
  5.  
  6. let a: any = { id: '1', status: 3 }; 
  7.  
  8. let b: DataModel = a as DataModel; 

在存取 property 時都還是 a 的資料型態,這樣在調用該型態的方法時就會出錯。

為了型態的正確必須進行轉換:

  1. let b: DataModel = <DataModel>{ 
  2.    id: Number(a.id), 
  3.    status: String(a.status), 
  4. }; 
2023-03-08 16:45

[Angular] 簡單製作多國語系

我想要用一種簡單而且直覺的方式製作多國語系,想像的用法如下:

user-list.component.html

  1. <h2>{{lang['T_編輯使用者']}}</h2> 
  2.  
  3. <div> 
  4.    <p *ngIf="lang.id == 'en'"> 
  5.    Complicated English description... 
  6.    </p> 
  7.    <p *ngIf="lang.id == 'tw'"> 
  8.    複雜的中文描述... 
  9.    </p> 
  10. </div> 
  11.  
  12. <button type="submit">{{lang['B_儲存']}}</button> 
  13.  
  14. <select [(ngModel)]="lang.id" (change)="lang.change()"> 
  15.    <option *ngFor="let x of lang.options | keyvalue" [value]="x.key">{{x.value}}</option> 
  16. </select> 

我希望能使用中文變數,有熟悉的文字可以讓程式看起來親切一點,對於複雜的文字區段也保留彈性,簡單的切換方式,以及狀態保留,讓 browser 紀錄選擇的語系。

user-list.component.ts

  1. import { Component, OnInit } from '@angular/core'; 
  2. import { AppLang } from 'app/app.lang'; 
  3.  
  4. @Component({ 
  5.    selector: 'app-user-list', 
  6.    templateUrl: './user-list.component.html' 
  7. }) 
  8. export class UserListComponent implements OnInit { 
  9.  
  10.    /** 狀態選項 */ 
  11.    statusItems = { 
  12.        'y': this.lang.E_啟用, 
  13.        'n': this.lang.E_停用, 
  14.    }; 
  15.  
  16.    constructor( 
  17.        public lang: AppLang 
  18.    ) { } 
  19.  
  20.    ngOnInit() { 
  21.        let msg = String(this.lang.M_請填寫_0_的刻度值).format(3); 
  22.    } 
  23. } 

已經想好使用情境了,接著來就是完成實作了!

app.lang.ts

  1. import { Injectable } from '@angular/core'; 
  2.  
  3. /** 多國語言 */ 
  4. @Injectable() 
  5. export class AppLang { 
  6.  
  7.    constructor() { 
  8.        let _: any = {}; /* 語系字典 */ 
  9.        let self: any = this; 
  10.        let names: string[] = Object.keys(this).filter(n => !'id,options,change'.includes(n)); 
  11.  
  12.        /* 重組語系字典 */ 
  13.        Object.keys(this.options).forEach(id => { 
  14.            _[id] = {}; 
  15.            names.forEach(name => _[id][name] = self[name][id]); 
  16.        }); 
  17.  
  18.        /* 複寫 change */ 
  19.        this.change = function () { 
  20.            if (!_[this.id]) { this.id = 'tw'; } 
  21.  
  22.            Object.assign(this, _[this.id]); 
  23.            localStorage['langId'] = this.id; 
  24.        } 
  25.  
  26.        this.change (); 
  27.    } 
  28.  
  29.  
  30.    id: string = localStorage['langId']; 
  31.  
  32.    options: any = { 
  33.        tw: '中文', 
  34.        en: 'English', 
  35.    }; 
  36.  
  37.    /** 變更語言 */ 
  38.    change() { } 
  39.  
  40.  
  41.  
  42.    /*=[ Title ]=============================================*/ 
  43.  
  44.    T_編輯使用者: any = { 
  45.        tw: '編輯使用者', 
  46.        en: 'Edit User', 
  47.    }; 
  48.    //... 
  49.  
  50.    /*=[ Field ]=============================================*/ 
  51.  
  52.    F_名稱: any = { 
  53.        tw: '名稱', 
  54.        en: 'Name', 
  55.    }; 
  56.    //... 
  57.  
  58.    /*=[ Button ]=============================================*/ 
  59.  
  60.    B_儲存: any = { 
  61.        tw: '儲存', 
  62.        en: 'Save', 
  63.    }; 
  64.    //... 
  65.  
  66.  
  67.    /*=[ Enum ]=============================================*/ 
  68.  
  69.    E_啟用: any = { 
  70.        tw: '啟用', 
  71.        en: 'Enable', 
  72.    }; 
  73.    E_停用: any = { 
  74.        tw: '停用', 
  75.        en: 'Disable', 
  76.    }; 
  77.    //... 
  78.  
  79.  
  80.    /*=[ Message ]=============================================*/ 
  81.  
  82.    M_請填寫_0_的刻度值: any = { 
  83.        tw: '請填寫 {0} 的刻度值', 
  84.        en: 'Please fill in the scale value of {0}.', 
  85.    }; 
  86.    //... 
  87.  
  88. } 

app.module.ts 配置

  1. //... 
  2. import { AppLang } from 'app/app.lang'; 
  3.  
  4. @NgModule({ 
  5.    declarations: [ 
  6.        //... 
  7.    ], 
  8.    imports: [ 
  9.        //... 
  10.    ], 
  11.    providers: [ 
  12.        //... 
  13.        AppLang 
  14.    ], 
  15.    bootstrap: [AppComponent] 
  16. }) 
  17. export class AppModule { } 
2023-03-08 14:21

[Angular] 將 select, radio 改成一般等於 (==)

Angular 預設的 select, radio 比對是用全等於(===),這對用 string 的選項來說沒甚麼問題,但如果是數字選項就會出現型態不一致的問題,就必須型態轉換,這有點煩人,所以想要改成一般等於(==),在 Angular 的原始碼找到可以複寫判斷句的地方。

app.module.ts

  1. import { RadioControlValueAccessor, SelectControlValueAccessor } from '@angular/forms'; 
  2.  
  3. /* 將 SelectControlValueAccessor 的全等於改成一般等於 */ 
  4. SelectControlValueAccessor.prototype.compareWith = function (a, b) { 
  5.    return a == b; 
  6. }; 
  7.  
  8. /* 將 RadioControlValueAccessor 的全等於改成一般等於 */ 
  9. RadioControlValueAccessor.prototype.writeValue = function (value: any) { 
  10.    let self: any = this; 
  11.    self._state = value == this.value; 
  12.    self._renderer.setProperty(self._elementRef.nativeElement, 'checked', self._state); 
  13. }; 
2023-03-07 16:55

[ThreeJS] 單次動畫

  1. /* 單次動畫 */ 
  2. function animateOnce(mixer, prop, dur, values) { 
  3.    /* 停止 & 清除 先前的動畫 */ 
  4.    mixer.stopAllAction(); 
  5.    mixer.uncacheRoot(mixer.getRoot()); 
  6.  
  7.    /* 增加動畫片段 */ 
  8.    const track = new THREE.KeyframeTrack(prop, [0, dur], values); 
  9.    const clip = new THREE.AnimationClip('move', dur, [track]); 
  10.    const action = mixer.clipAction(clip); 
  11.    action.clampWhenFinished = true; 
  12.    action.setLoop(THREE.LoopOnce); 
  13.  
  14.    /* Promise 來處理完成事件,這樣就可以用 await */ 
  15.    return new Promise((resolve) => { 
  16.        const finished = function () { 
  17.            mixer.removeEventListener('finished', finished); 
  18.            resolve(); 
  19.        }; 
  20.        mixer.addEventListener('finished', finished); 
  21.  
  22.        action.play(); /* 播放動畫 */ 
  23.    }); 
  24. } 

想要能像 SVG 一樣簡單的控制模型的移動,ThreeJS 並沒有直接提供我想要的模式,試了很久終於成功了。

mixer 中的 action 是不斷添加的,這會混亂播放動畫,所以在播放新的動畫前必須[停止]且[清除]先前的動畫。

為了可以簡單串接不同模型的動畫,所以就想用 await 來達成,所以加上 Promise 的包裝。

完整程式

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4. renderer.setPixelRatio(window.devicePixelRatio); 
  5. renderer.setSize(window.innerWidth, window.innerHeight); 
  6.  
  7. /* 初始化場景 */ 
  8. const scene = new THREE.Scene(); 
  9. scene.background = new THREE.Color('#000'); /* 背景顏色 */ 
  10. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  11. scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */ 
  12.  
  13. /* 初始化鏡頭 */ 
  14. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  15. camera.position.set(0, 4, 12); 
  16.  
  17. /* 初始化軌道控制,鏡頭的移動 */ 
  18. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  19. orbitControls.update(); 
  20.  
  21.  
  22. /* 動畫刷新器 */ 
  23. const mixers = []; 
  24.  
  25. function newMixer(target) { 
  26.    const mixer = new THREE.AnimationMixer(target); 
  27.    mixers.push(mixer); 
  28.    return mixer; 
  29. } 
  30.  
  31. /* 動畫計時器 */ 
  32. const clock = new THREE.Clock(); 
  33.  
  34. /* 渲染週期 */ 
  35. function renderCycle() { 
  36.    const delta = clock.getDelta(); 
  37.    mixers.forEach(x => x.update(delta)); 
  38.  
  39.    renderer.render(scene, camera); 
  40.    requestAnimationFrame(renderCycle); 
  41. } 
  42. renderCycle(); 
  43.  
  44.  
  45.  
  46. /*---------------------------------------------------------------*/ 
  47.  
  48. /* 單次動畫 */ 
  49. function animateOnce(mixer, prop, dur, values) { 
  50.    /* 停止 & 清除 先前的動畫 */ 
  51.    mixer.stopAllAction(); 
  52.    mixer.uncacheRoot(mixer.getRoot()); 
  53.  
  54.    /* 增加動畫片段 */ 
  55.    const track = new THREE.KeyframeTrack(prop, [0, dur], values); 
  56.    const clip = new THREE.AnimationClip('move', dur, [track]); 
  57.    const action = mixer.clipAction(clip); 
  58.    action.clampWhenFinished = true; 
  59.    action.setLoop(THREE.LoopOnce); 
  60.  
  61.    /* Promise 來處理完成事件,這樣就可以用 await */ 
  62.    return new Promise((resolve) => { 
  63.        const finished = function () { 
  64.            mixer.removeEventListener('finished', finished); 
  65.            resolve(); 
  66.        }; 
  67.        mixer.addEventListener('finished', finished); 
  68.  
  69.        action.play(); /* 播放動畫 */ 
  70.    }); 
  71. } 
  72.  
  73.  
  74. const cubeA = new THREE.Mesh( 
  75.    new THREE.BoxGeometry(1, 1, 1), 
  76.    new THREE.MeshBasicMaterial( {color: '#0F0'} ) 
  77. ); 
  78. const mixerA = newMixer(cubeA); 
  79. scene.add(cubeA); 
  80.  
  81.  
  82. const cubeB = new THREE.Mesh( 
  83.    new THREE.BoxGeometry(1, 1, 1), 
  84.    new THREE.MeshBasicMaterial( {color: '#00F'} ) 
  85. ); 
  86. const mixerB = newMixer(cubeB); 
  87. scene.add(cubeB); 
  88.  
  89.  
  90. async function run() { 
  91.    await animateOnce(mixerA, '.position[x]', 3, [0, 3]); 
  92.    await animateOnce(mixerB, '.position[y]', 3, [0, 2]); 
  93.    await animateOnce(mixerA, '.position[x]', 3, [3, 0]); 
  94.    await animateOnce(mixerB, '.position[y]', 3, [2, 0]); 
  95. } 
  96.  
  97. run(); 
2023-03-07 16:06

[ThreeJS] 用 SVG 貼圖顯示中文

HTML

  1. <div id="Container"></div> 
  2.  
  3. <div id="LabelTpl" style="display:none;"> 
  4.    <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 30 30"> 
  5.        <text x="15" y="15" fill="#fff" text-anchor="middle"></text> 
  6.    </svg> 
  7. </div> 
  8.  
  9. <script src="../Scripts/three.145/three.min.js"></script> 
  10. <script src="../Scripts/three.145/controls/OrbitControls.js"></script> 
  11. <script src="../Scripts/three.145/loaders/GLTFLoader.js"></script> 
  12. <script src="main.js"></script> 

main.js

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4.  
  5. renderer.setPixelRatio(window.devicePixelRatio); 
  6. renderer.setSize(window.innerWidth, window.innerHeight); 
  7.  
  8.  
  9. /* 初始化場景 */ 
  10. const scene = new THREE.Scene(); 
  11. scene.background = new THREE.Color('#000'); /* 背景顏色 */ 
  12. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  13. scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */ 
  14.  
  15. /* 初始化鏡頭 */ 
  16. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  17. camera.position.set(0, 4, 12); 
  18.  
  19.  
  20. /* 初始化軌道控制,鏡頭的移動 */ 
  21. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  22. orbitControls.update(); 
  23.  
  24.  
  25. /* 渲染週期 */ 
  26. function renderCycle() { 
  27.    renderer.render(scene, camera); 
  28.    requestAnimationFrame(renderCycle); 
  29. } 
  30. renderCycle(); 
  31.  
  32.  
  33.  
  34. /*---------------------------------------------------------------*/ 
  35.  
  36. function svgBase64(svg) { 
  37.    svg = svg.replace(/\s+</g, '<').replace(/>\s+/g, '>'); 
  38.    return 'data:image/svg+xml,' + encodeURIComponent(svg); 
  39. } 
  40.  
  41.  
  42. const textureLoader = new THREE.TextureLoader(); 
  43. const $labelTpl = document.querySelector('#LabelTpl'); 
  44. const $labelText = document.querySelector('#LabelTpl text'); 
  45.  
  46.  
  47. /* 環線 */ 
  48. function addCircle(label,  z) { 
  49.    $labelText.innerHTML = label; 
  50.    let imageData = svgBase64($labelTpl.innerHTML); 
  51.  
  52.    const particles = new THREE.Points( 
  53.        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 6)), 
  54.        new THREE.PointsMaterial({ 
  55.            map: textureLoader.load(imageData), 
  56.            color: '#FFF', 
  57.            size: 4, 
  58.            depthWrite: false, 
  59.            transparent: true, 
  60.        }) 
  61.    ); 
  62.    particles.position.z = z; 
  63.    scene.add(particles); 
  64.  
  65.  
  66.    const circle = new THREE.LineSegments( 
  67.        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 60)), 
  68.        new THREE.LineBasicMaterial({ color: '#FFF' }) 
  69.    ); 
  70.    circle.position.z = z; 
  71.    scene.add(circle); 
  72. } 
  73.  
  74. addCircle('冬至', 4); 
  75. addCircle('夏至', -4); 
2023-03-07 13:01

[ThreeJS] 解決陰影造成的條紋

 會出現條紋現象是因為雙面材質,只要設置為單面材質就可以解決。

  1. /* 載入模型 */ 
  2. new THREE.GLTFLoader().load('Model.glb', function (gltf) { 
  3.    const model = gltf.scene; 
  4.  
  5.    model.traverse(obj => { 
  6.        if (!obj.isMesh) { return; } 
  7.  
  8.        obj.frustumCulled = false; 
  9.        obj.castShadow = true; 
  10.        obj.receiveShadow = true; 
  11.  
  12.        /* 解決陰影造成的條紋 */ 
  13.        obj.material.side = THREE.FrontSide; 
  14.        obj.material.shadowSide = THREE.FrontSide; 
  15.    }); 
  16.  
  17.    scene.add(model); 
  18. }); 
2023-03-07 12:49

[ThreeJS] DragControls 拖移出現異常的原因

 需要進行拖移的模型其[原點]最好設置在[幾何中心]

因為拖移出現異常,我為了解決這個問題,認真看 DragControls 的原始碼,所有處理拖移的邏輯都相當周全,基本上不應該出現異常現象。

在用 console.log 查看拖移的座標數值時,才發現原來是模型的[原點]設置在[場景中心]造成的。

2023-03-07 11:37

[ThreeJS] 載入壓縮過 GLTF 檔

  1. /* 解壓縮 lib 載入器 
  2. * https://threejs.org/docs/#examples/en/loaders/DRACOLoader 
  3. * https://google.github.io/draco/ 
  4. */ 
  5. const dracoLoader = new THREE.DRACOLoader(); 
  6.  
  7. /* 指定解壓器的位置 */ 
  8. dracoLoader.setDecoderPath('../Scripts/three.145/libs/draco/'); 
  9.  
  10.  
  11. /* GLTF 載入器 */ 
  12. const gltfLoader = new THREE.GLTFLoader(); 
  13. gltfLoader.setDRACOLoader(dracoLoader); 
  14.  
  15. /* 載入模型 */ 
  16. gltfLoader.load('Model.glb', function (gltf) { 
  17.    const model = gltf.scene; 
  18.    scene.add(model); 
  19. }); 
2023-03-03 15:08

[ThreeJS] 材質發黑是因為金屬屬性

剛開始接觸 ThreeJS 時,被材質發黑這個問題,困擾很久,一直沒找到問題點,不管怎麼增加 HemisphereLight 都無效,用了四個方向 DirectionalLight 才呈現能接受的效果。

後來才發現問題點是金屬屬性 (metalness),因為金屬有鏡面效果會映像出周圍的景象,然後我又沒有設置 environment map,整個場景 (scene) 都是黑漆漆的,所以材質映像出來也會是黑色的。