**本篇文章適合對react有基本的認識,如果了解three.js及r3f更好** [CodeSandBox 看效果](https://codesandbox.io/p/sandbox/romantic-dew-dvflhr?file=%2Fsrc%2FTransparentVideo%2FTransparentVideo.tsx%3A11%2C35) ## 範例 ![](https://hackmd.io/_uploads/rkSFkas5h.png) ## 素材 ![](https://hackmd.io/_uploads/S14G1pscn.png) 首先,需要有類似這樣的影片當作素材,上半部是影片本人,下半部則是白底的影片 [影片素材](https://video.wixstatic.com/video/11062b_92ee532ce1f74d77b5b1fcd7dc1c40b8/480p/mp4/file.mp4) ## React 3 Fiber(R3F) R3F是一個基於three.js的React library,他的功能是將three.js的封裝成jsx及hooks,以便在react框架中作使用。 [官方文件](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) > 問:為什麼單純放個影片需要用到3D的框架? > 答:支援度,Chrome 支援的webM 在 safari 瀏覽器還沒支援 ## 第一步:建立 R3F 的 Canvas ``` import {Canvas} from '@react-three/fiber'; // 由 r3f 提供的 <Canvas> 已經帶入許多預設的參數 // children 則必須是 <mesh> 或其他three.js需要用到的元件 const Test = (children) => { return ( <Canvas>{children}</Canvas> ); }; ``` 如果是原生three.js,這邊會需要帶入相當多基礎設定,這邊就不多做提及,可參閱[前端如何制作出透明背景视频](http://t.csdn.cn/C5K4Q) 中原生的寫法 [Canvas的預設props](https://docs.pmnd.rs/react-three-fiber/api/canvas) ## 第二步:建立影片來源 ``` <video src="./video.mp4" autoPlay={true} muted={true} loop={true}></video> ``` 主流瀏覽器為了更良好的使用者體驗,在播放影片時有許多限制,簡單來說如果要自動播放就必須設置靜音 ## 第三步:建立 R3F 的物件 這邊就屬於比較複雜的部分,three.js的世界中,所有3D的物件都是一個mesh,mesh必須包括兩樣東西 geometry(形狀) 及 material(紋理),今天這個情境不會深究這兩樣東西,因為我們的3D畫布只會有一個平面物件 材質來源則是影片。 ``` const VideoComponent = () => { return ( <mesh> // 平面物件 <planeGeometry> // 客製化 Shader 的紋理 <shaderMaterial uniforms={uniforms} vertexShader={vertexShader} fragmentShader={fragmentShader} /> </mesh> ) } ``` ## 第四步:Shader [Shader:圖像語言,簡單來說,就是想客製化材質,就自己寫](https://thebookofshaders.com/) [Uniform:GLSL的全域變數,three.js 與 shader 語言之間的橋樑](https://threejs.org/docs/?q=uniform#api/en/core/Uniform) 這邊學習成本極高,可以稍微看看就好,有興趣可以自行Google在寫什麼,附上chatGPT的解釋: > 這段程式碼是一個用於處理材質的片段着色器(fragment shader),用於在WebGL或OpenGL環境下渲染影片材質。讓我們逐步解釋這段程式碼: > > > 頂點着色器 > > 這部分定義了頂點着色器,它會將每個頂點的座標(position)轉換到屏幕空間的位置(gl_Position),並將材質的UV坐標(uv)傳遞到片段着色器中。這裡使用了varying修飾詞來將vUv變量從頂點着色器傳遞到片段着色器。 ``` export const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `; ``` > 片段着色器 > > 片段着色器是在每個像素上運行的程式碼,用於計算最終的顏色。它包含兩個主要部分,即樣本紋理(texture)的讀取和最終顏色的計算。 > > uniform sampler2D uTexture;:這是一個名為uTexture的2D紋理變量(**就是影片**),它將用於存儲傳入的影片材質。uniform表示這是一個全局變量,在渲染期間保持不變。 > > varying vec2 vUv;:這是從頂點着色器中傳遞過來的材質的UV坐標。它包含了每個像素在紋理上的位置。 > > vec4 textureColor = texture2D(uTexture, vec2(vUv.x, 0.5 + vUv.y/2.));:這行程式碼讀取影片紋理中位於(vUv.x, 0.5 + vUv.y/2.0)位置的顏色。其中texture2D函數將紋理坐標轉換為對應的顏色。 > > vec4 textureWhite = texture2D(uTexture, vec2(vUv.x, vUv.y/2.));:這行程式碼讀取影片紋理中位於(vUv.x, vUv.y/2.0)位置的顏色,用於產生白色的部分。 > > gl_FragColor = vec4(textureColor.xyz, textureWhite.x);:這行程式碼計算最終的顏色。它將textureColor中的RGB顏色(.xyz)與textureWhite的紅色(.x)組合起來,形成最終的片段顏色。其中,gl_FragColor是內建的變量,表示最終的片段顏色。 ``` export const fragmentShader = ` uniform sampler2D uTexture; varying vec2 vUv; void main() { vec4 textureColor = texture2D(uTexture, vec2(vUv.x, 0.5 + vUv.y/2.)); vec4 textureWhite = texture2D(uTexture, vec2(vUv.x, vUv.y/2.)); gl_FragColor = vec4(textureColor.xyz, textureWhite.x); } `; ``` ## 第五步驟:將影片傳給R3F的物件 ``` const VideoComponent = ({video}: IVideoComponentProps) => { if(video === null) return null; // 載入影片紋理 const videoTexture = new VideoTexture(video); // 將影片紋理透過uniforms傳給shaders const uniforms = { uTexture: { value: videoTexture } } //設定形狀大小與Canvas外觀相等,請看下方詳解 const {width, height} = useThree((state) => state.viewport) return ( <mesh> <planeGeometry args={[width, height, 1]}/> <shaderMaterial uniforms={uniforms} vertexShader={vertexShader} fragmentShader={fragmentShader}/> </mesh> ) } ``` [VideoTextrue:three.js 的loader,可以將video轉為material](https://threejs.org/docs/#api/en/textures/VideoTexture) [useThree:R3F 提供的 hook,主要是提供一些相關數據例如場景 相機等等](https://docs.pmnd.rs/react-three-fiber/api/hooks) 透過useThree解構出來的寬度以及高度,是屬於3D世界中的單位,不能直接設置100%, vw, px等,這個部份需要對three.js有基本的認識比較能理解,簡單來說,透過這樣的設置之後我們的影片mesh的寬高就等於canvas的寬高, 如下圖中的老虎: ![](https://hackmd.io/_uploads/r1ePrCoc3.png) ## 最後步驟:將mesh加入<Canvas></Canvas> ``` const TransparentVideo = ({filePath}: ITransparentVideoProps) => { const videoRef = useRef<TVideo>(null); const [isVideoReady, setIsVideoReady] = useState(false); useEffect(()=>{ if(videoRef) { setIsVideoReady(true); } }, [videoRef]) return ( <Container> <Video src={filePath} ref={videoRef} autoPlay={true} muted={true} loop={true}></Video> <Canvas gl={{antialias: false}}> {isVideoReady && <VideoComponent video={videoRef.current}/>} </Canvas> </Container> ); }; ``` 問:一定需要一個video標籤來當作影片來源嗎? 答:沒錯,透過video標籤的api,才能使用WebGL呈現出來 ### 你一定有個疑問,到底為什麼這樣就是透明? **這個部分chatGPT提供了滿完整的解釋,只是想要徹底理解還是必須學習 Shader的基礎** > 這段程式碼之所以可以呈現將影片中的黑色疊在一起就變成透明的效果,是因為> 它利用了片段着色器中對影片紋理的讀取和顏色計算方式。 > > 讓我們來仔細分析一下這段程式碼中的片段着色器部分: ``` uniform sampler2D uTexture; varying vec2 vUv; void main() { vec4 textureColor = texture2D(uTexture, vec2(vUv.x, 0.5 + vUv.y/2.)); vec4 textureWhite = texture2D(uTexture, vec2(vUv.x, vUv.y/2.)); gl_FragColor = vec4(textureColor.xyz, textureWhite.x); } ``` > textureColor 讀取了紋理中位於 (vUv.x, 0.5 + vUv.y/2.0) 位置的顏色。 > textureWhite 讀取了紋理中位於 (vUv.x, vUv.y/2.0) 位置的顏色。 > 根據這兩行程式碼,我們可以看到 textureColor 和 textureWhite 是影片紋理中不同位置的顏色。而後面的 gl_FragColor 是最終片段的顏色。 > > 影片紋理在這段程式碼中被用來實現「黑色疊加變透明」的效果,關鍵在於 textureWhite 的紅色分量(.x)。 > > 如果 textureWhite 的紅色分量為1(即純白色),則最終 gl_FragColor 的紅色部分為1,即完全不透明。 > 如果 textureWhite 的紅色分量為0(即純黑色),則最終 gl_FragColor 的紅色部分為0,即完全透明。 > 因此,片段着色器中的這段程式碼相當於控制了影片紋理中的哪些部分是透明的(黑色部分),哪些部分是不透明的(非黑色部分)。這樣做的效果就是將影片中的黑色部分疊加在一起,使其變成透明的,從而達到特定的視覺效果。 > > 請注意,這段程式碼只是示例,具體效果可能取決於影片紋理的內容以及應用程式中其他部分的處理。在實際應用中,您可以通過調整程式碼和紋理來獲得所需的效果。 ## 使用套件 ``` "@react-three/fiber": "^8.13.5", "react": "^18.2.0", "react-dom": "^18.2.0", "styled-components": "^6.0.4", "three": "^0.154.0" ```