threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

shadertoy-player.md 16KB

11 miesięcy temu
11 miesięcy temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. ---
  2. prev:
  3. text: 'Setting Background Color and Images'
  4. link: './scene-background'
  5. next:
  6. text: 'Using Vanilla Three.js style code'
  7. link: './vanilla-threejs'
  8. aside: false
  9. ---
  10. # ShaderToy Shaders in Three.js
  11. This tutorial shows how to use shaders from ShaderToy in a Three.js scene by using them as custom screen shaders. You'll learn how to run ShaderToy shaders in a Three.js context, pass uniforms, and create interactive controls.
  12. <iframe src="https://threepipe.org/examples/shadertoy-player/" style="width:100%;height:600px;border:none;"></iframe>
  13. ## Overview
  14. ShaderToy is a popular online shader editor that uses a specific format for fragment shaders. To use these shaders in Three.js, we need to:
  15. 1. Set up the proper uniforms that ShaderToy expects
  16. 2. Create a custom material that wraps the ShaderToy shader
  17. 3. Use it as a screen shader in threepipe
  18. 4. Handle mouse input and time updates
  19. ## Step 1: Setting Up the Basic Structure
  20. First, let's create a new project or install `threepipe`, `@threepipe/plugin-tweakpane`(into existing project) and import the necessary modules in your project:
  21. ```bash
  22. # If you are starting a new project, use the following command to create a new threepipe project
  23. npm create threepipe@latest
  24. # If you are adding to an existing project, install threepipe and the Tweakpane UI plugin
  25. npm install threepipe @threepipe/plugin-tweakpane
  26. ```
  27. Import the required modules in your JavaScript or TypeScript file:
  28. ```typescript
  29. import {
  30. ExtendedShaderMaterial,
  31. glsl,
  32. GLSL3,
  33. LoadingScreenPlugin,
  34. MaterialExtension,
  35. ThreeViewer,
  36. Vector2,
  37. Vector3,
  38. Vector4,
  39. } from 'threepipe'
  40. import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
  41. ```
  42. ::: tip
  43. Checkout the [Quickstart section](./../guide/getting-started#quickstart) for more details on how to set up a basic threepipe project.
  44. :::
  45. ## Step 2: Define ShaderToy Uniforms
  46. ShaderToy shaders expect specific uniforms. Create these to match the ShaderToy specification:
  47. ```typescript
  48. const uniforms = {
  49. iResolution: {value: new Vector3()}, // viewport resolution
  50. iTime: {value: 0}, // shader playback time
  51. iFrame: {value: 0}, // current frame number
  52. iMouse: {value: new Vector4()}, // mouse pixel coords
  53. iTimeDelta: {value: 0}, // render time delta
  54. iDate: {value: new Vector4()}, // current date
  55. iFrameRate: {value: 0}, // frame rate
  56. iChannel0: {value: null}, // texture channels
  57. iChannel1: {value: null},
  58. iChannel2: {value: null},
  59. iChannel3: {value: null},
  60. // Additional uniforms for channel sizes
  61. iChannel0Size: {value: new Vector2()},
  62. iChannel1Size: {value: new Vector2()},
  63. iChannel2Size: {value: new Vector2()},
  64. iChannel3Size: {value: new Vector2()},
  65. // Custom uniforms for shader parameters
  66. customFloat: {value: 0.5}, // Sample float parameter
  67. customColor: {value: new Vector3(1.0, 0.5, 0.2)}, // Sample color parameter
  68. customIntensity: {value: 1.0}, // Sample intensity parameter
  69. }
  70. ```
  71. ### Adding Custom Parameters
  72. You can extend the uniforms object with any custom parameters your shader needs. These will be available in your fragment shader and can be controlled through the UI. Common types include:
  73. - **Float values**: For controlling intensity, speed, scale, etc.
  74. - **Vector3 colors**: For color parameters
  75. - **Vector2/Vector3/Vector4**: For vectors
  76. - **Boolean flags**: For enabling/disabling effects (passed as floats: 0.0 or 1.0)
  77. ## Step 3: Create the Shader Material
  78. The fragment shader needs to be adapted to work with Three.js. Here's the wrapper that makes ShaderToy shaders compatible:
  79. ```glsl
  80. precision highp int;
  81. precision highp sampler2D;
  82. uniform vec3 iResolution;
  83. uniform float iTime;
  84. uniform vec4 iMouse;
  85. uniform vec4 iDate;
  86. uniform float iTimeDelta;
  87. uniform int iFrame;
  88. uniform float iFrameRate;
  89. // Channel uniforms
  90. uniform vec2 iChannel0Size;
  91. uniform vec2 iChannel1Size;
  92. uniform vec2 iChannel2Size;
  93. uniform vec2 iChannel3Size;
  94. in vec2 vUv;
  95. layout(location = 0) out vec4 glFragColor;
  96. void main() {
  97. // Set up channel resolutions
  98. vec3 iChannelResolution[4];
  99. iChannelResolution[0] = vec3(iChannel0Size, 1.0);
  100. iChannelResolution[1] = vec3(iChannel1Size, 1.0);
  101. iChannelResolution[2] = vec3(iChannel2Size, 1.0);
  102. iChannelResolution[3] = vec3(iChannel3Size, 1.0);
  103. // Call the ShaderToy main function
  104. mainImage(glFragColor, gl_FragCoord.xy);
  105. // Apply screen shader processing
  106. vec4 diffuseColor = glFragColor;
  107. #glMarker
  108. glFragColor = diffuseColor;
  109. // Ensure alpha is 1.0 for screen shaders
  110. glFragColor.a = 1.0;
  111. }
  112. ```
  113. The shader calls the `mainImage` function, which is where your ShaderToy code will go. This function should be defined in your ShaderToy code and will receive the `fragColor` and `fragCoord` parameters.
  114. Since this is not defined in the shader itself, it will not compile without a material extension that injects the `mainImage` function.
  115. ::: note `glMarker`
  116. The `#glMarker` directive is a placeholder for the `ScreenPass` in threepipe that indicates where the screen shader extensions should be added. These include extensions by plugins like `TonemapPlugin`, `VignettePlugin`, etc.
  117. You can remove it if you don't need these extensions.
  118. `diffuseColor` is the final color output of the shader, which will be modified by the screen shader extensions.
  119. Checkout the [Screen Pass guide](./../guide/screen-pass) for more information on how screen shaders work in threepipe.
  120. :::
  121. ## Step 4: Create the Material Extension
  122. Use a `MaterialExtension` to inject your ShaderToy code:
  123. ```typescript
  124. const toyExtension: MaterialExtension = {
  125. parsFragmentSnippet: `
  126. unfiform float customFloat;
  127. uniform vec3 customColor;
  128. uniform float customIntensity;
  129. void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  130. vec2 uv = fragCoord / iResolution.xy;
  131. fragColor = vec4(uv * customIntensity, 0, 1);
  132. }
  133. `,
  134. isCompatible: () => true,
  135. computeCacheKey: Math.random().toString(),
  136. }
  137. ```
  138. Here, `parsFragmentSnippet` is added to the material's fragment shader just before the main function.
  139. You can replace it with your ShaderToy code with any custom uniforms you need.
  140. Checkout the [Material Extension guide](./../guide/material-extension) for more information.
  141. This has a default shader, but you can dynamically change the `parsFragmentSnippet` to load different ShaderToy shaders at runtime.
  142. ```typescript
  143. const response = await fetch('https://asset-samples.threepipe.org/shaders/tunnel-cylinders.glsl')
  144. const shaderText = await response.text()
  145. toyExtension.parsFragmentSnippet = v
  146. toyExtension.computeCacheKey = Math.random().toString()
  147. material.setDirty()
  148. ```
  149. ## Step 5: Set Up the Material and Viewer
  150. Create the [`ExtendedShaderMaterial`](https://threepipe.org/docs/classes/ExtendedShaderMaterial.html) and configure the viewer:
  151. ```typescript
  152. const material = new ExtendedShaderMaterial({
  153. uniforms: uniforms,
  154. defines: {
  155. IS_SCREEN: '1',
  156. IS_LINEAR_OUTPUT: '1',
  157. },
  158. glslVersion: GLSL3,
  159. vertexShader: toyVert,
  160. fragmentShader: toyFrag,
  161. transparent: true,
  162. depthTest: false,
  163. depthWrite: false,
  164. premultipliedAlpha: false,
  165. })
  166. material.registerMaterialExtensions([toyExtension])
  167. const viewer = new ThreeViewer({
  168. canvas: document.getElementById('mcanvas'),
  169. msaa: false,
  170. rgbm: false,
  171. tonemap: false,
  172. screenShader: material, // Use as screen shader/material
  173. renderScale: 2,
  174. })
  175. ```
  176. The material is set as the `screenShader` in the viewer configuration, which sets it as material in the `ScreenPass`. Check out the [Screen Pass guide](./../guide/screen-pass) for more details on how custom screen shaders/materials work in threepipe.
  177. ::: note `ExtendedShaderMaterial`
  178. [`ExtendedShaderMaterial`](https://threepipe.org/docs/classes/ExtendedShaderMaterial.html) is a custom material that allows dynamic shader code injection and supports the `MaterialExtension` system.
  179. It extends the standard `ShaderMaterial` to provide additional features like automatic uniform management, shader code injection, compatibility with the threepipe material extension system, and automatic texture encoding and size support.
  180. It is used here to apply the ShaderToy shader as a screen shader in the viewer.
  181. :::
  182. ## Step 6: Handle Time and Frame Updates
  183. Update the uniforms each frame to animate the shader:
  184. ```typescript
  185. viewer.addEventListener('preFrame', (ev) => {
  186. if (!params.running && !params.stepFrame) return
  187. // Update time uniforms
  188. uniforms.iTimeDelta.value = (ev.deltaTime || 0) / 1000.0
  189. params.time += uniforms.iTimeDelta.value
  190. uniforms.iTime.value = params.time
  191. uniforms.iFrame.value = params.frame++
  192. // Update date
  193. const date = new Date()
  194. uniforms.iDate.value.set(
  195. date.getFullYear(),
  196. date.getMonth(),
  197. date.getDate(),
  198. date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds()
  199. )
  200. // Update resolution
  201. const bufferSize = [
  202. viewer.renderManager.renderSize.width * viewer.renderManager.renderScale,
  203. viewer.renderManager.renderSize.height * viewer.renderManager.renderScale
  204. ]
  205. uniforms.iResolution.value.set(bufferSize[0], bufferSize[1], 1)
  206. material.uniformsNeedUpdate = true
  207. viewer.setDirty()
  208. })
  209. ```
  210. ## Step 7: Handle Mouse Input
  211. Implement mouse tracking to match ShaderToy's mouse behavior:
  212. ```typescript
  213. const mouse = {
  214. position: new Vector2(),
  215. clickPosition: new Vector2(),
  216. isDown: false,
  217. isClick: false,
  218. }
  219. function getMouseFromEvent(canvas, e) {
  220. const rect = canvas.getBoundingClientRect()
  221. const x = e.clientX - rect.left
  222. const y = e.clientY - rect.top
  223. if (x < 0 || y < 0 || x > rect.width || y > rect.height) return null
  224. return mouse.position.set(x / rect.width, 1.0 - y / rect.height)
  225. }
  226. // Update mouse uniform in preFrame event
  227. uniforms.iMouse.value.set(
  228. mouse.position.x * bufferSize[0],
  229. mouse.position.y * bufferSize[1],
  230. mouse.clickPosition.x * (mouse.isDown ? 1 : -1) * bufferSize[0],
  231. mouse.clickPosition.y * (mouse.isClick ? 1 : -1) * bufferSize[1]
  232. )
  233. ```
  234. Checkout the example code for the full boilderplate for mouse handling, including adding event listeners for `mousedown`, `mouseup`, and `mousemove` to update the `mouse` object.
  235. ## Step 8: Add Interactive Controls
  236. Create a UI to control the shader, including custom uniform parameters:
  237. ```typescript
  238. // Define parameters object to store UI-controlled values
  239. const params = {
  240. resolution: new Vector2(1280, 720),
  241. time: 0,
  242. frame: 0,
  243. running: true,
  244. stepFrame: false,
  245. // Custom parameters
  246. customFloat: 0.5,
  247. customColor: new Color(),
  248. customIntensity: 1.0,
  249. }
  250. const ui = {
  251. label: 'Shader Controls',
  252. type: 'folder',
  253. expanded: true,
  254. value: params,
  255. children: [{
  256. type: 'button',
  257. label: () => params.running ? 'Pause' : 'Play',
  258. onClick: () => {
  259. params.running = !params.running
  260. },
  261. }, {
  262. type: 'button',
  263. label: 'Reset',
  264. onClick: () => {
  265. params.frame = 0
  266. params.time = 0
  267. },
  268. }, {
  269. type: 'folder',
  270. label: 'Custom Parameters',
  271. expanded: true,
  272. children: [{
  273. type: 'slider',
  274. path: 'customFloat',
  275. label: 'Sample Float',
  276. bounds: [0, 1],
  277. stepSize: 0.01,
  278. onChange: () => {
  279. uniforms.customFloat.value = params.customFloat
  280. material.uniformsNeedUpdate = true
  281. viewer.setDirty()
  282. },
  283. }, {
  284. type: 'color',
  285. path: 'customColor',
  286. label: 'Sample Color',
  287. onChange: () => {
  288. uniforms.customColor.value.set(
  289. params.customColor.r,
  290. params.customColor.g,
  291. params.customColor.b
  292. )
  293. material.uniformsNeedUpdate = true
  294. viewer.setDirty()
  295. },
  296. }, {
  297. type: 'slider',
  298. path: 'customIntensity',
  299. label: 'Intensity',
  300. bounds: [0, 3],
  301. stepSize: 0.1,
  302. onChange: () => {
  303. uniforms.customIntensity.value = params.customIntensity
  304. material.uniformsNeedUpdate = true
  305. viewer.setDirty()
  306. },
  307. }],
  308. }, {
  309. type: 'button',
  310. label: 'Edit Shader',
  311. onClick: () => setupShaderEditor(toyExtension.parsFragmentSnippet, setShader),
  312. }],
  313. }
  314. const uiPlugin = viewer.addPluginSync(new TweakpaneUiPlugin(true))
  315. uiPlugin.appendChild(ui)
  316. ```
  317. ### UI Control Types
  318. The TweakpaneUiPlugin supports various control types for different uniform parameters:
  319. - **slider**: For numeric values with min/max bounds
  320. - **color**: For RGB color values (automatically converts to Vector3)
  321. - **button**: For triggering actions
  322. - **checkbox**: For boolean values
  323. - **folder**: For grouping related controls
  324. - **vec**: For Vector2/Vector3/Vector4 values (like resolution)
  325. ### Connecting UI to Uniforms
  326. Each UI control should have an `onChange` callback that:
  327. 1. Updates the corresponding uniform value
  328. 2. Sets `material.uniformsNeedUpdate = true`
  329. 3. Calls `viewer.setDirty()` to trigger a re-render
  330. ## Step 9: Dynamic Shader Loading
  331. Implement a function to update the shader dynamically:
  332. ```typescript
  333. const setShader = (shaderCode) => {
  334. toyExtension.parsFragmentSnippet = shaderCode
  335. toyExtension.computeCacheKey = Math.random().toString()
  336. material.setDirty()
  337. viewer.setDirty()
  338. }
  339. // Load a shader from URL
  340. const response = await fetch('path/to/shader.glsl')
  341. const shaderText = await response.text()
  342. setShader(shaderText)
  343. ```
  344. ## Texture Channels
  345. Textures can be set in the uniforms for the shader channels, or can be added to the UI config to configure dynamically:
  346. ```typescript
  347. uniforms.iChannel0.value = await viewer.load('path/to/texture0.png')
  348. // ...
  349. // sample to add to the UI
  350. const uiConfig = {
  351. type: 'image',
  352. property: [uniforms.iChannel0, 'value'],
  353. label: 'iChannel0',
  354. onChange: ()=>{
  355. material.uniformsNeedUpdate = true
  356. material.setDirty()
  357. }
  358. }
  359. ui.appendChild(uiConfig)
  360. ```
  361. ## ShaderToy post-processing
  362. Since the material is added as the `screenShader`, it is rendered in `ScreenPass` after other passes like `RenderPass`, etc.
  363. Output of these can be used to in the shader toy shader to blend the 3d scene with a custom shadertoy effect.
  364. This can be done by defining and accessing the `tDiffuse` and `tTransparent` uniforms in the material and shader code.
  365. Check out the [ScreenPass.glsl](https://github.com/repalash/threepipe/blob/master/src/postprocessing/ScreenPass.glsl) for a sample of how to access these textures in the shader code, as well as interfacing with the gbuffer.
  366. Check the [Screen Pass guide](./../guide/screen-pass) for more details and an example.
  367. ## Key Points
  368. 1. **Screen Shader Usage**: The material is used as a `screenShader` in the viewer configuration, which applies it as a post-processing effect.
  369. 2. **Uniform Management**: All ShaderToy uniforms must be properly updated each frame for the shader to work correctly.
  370. 3. **Coordinate Systems**: Pay attention to coordinate system differences between ShaderToy and Three.js, especially for mouse coordinates.
  371. 4. **Performance**: Screen shaders run on every pixel, so complex shaders can impact performance significantly.
  372. 5. **Material Extensions**: Using MaterialExtension allows dynamic shader code injection without recreating the entire material.
  373. This setup provides a complete ShaderToy player that can run most ShaderToy shaders with proper time, mouse, and resolution handling, plus interactive controls for experimentation.
  374. Check out the [live example](https://threepipe.org/examples/shadertoy-player/) to see it in action along with the source code on [GitHub](https://github.com/repalash/threepipe/tree/master/examples/shadertoy-player/script.ts).