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.

script.ts 15KB

11 miesięcy temu
11 miesięcy temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import {
  2. _testFinish,
  3. _testStart,
  4. CanvasSnapshotPlugin, createStyles, css,
  5. ExtendedShaderMaterial,
  6. glsl,
  7. GLSL3,
  8. LoadingScreenPlugin, MaterialExtension,
  9. ThreeViewer,
  10. UiObjectConfig,
  11. Vector2,
  12. Vector3,
  13. Vector4,
  14. } from 'threepipe'
  15. import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
  16. // import {BlueprintJsUiPlugin} from '@threepipe/plugin-blueprintjs'
  17. // Checkout the code breakup and explanation here - https://threepipe.org/notes/shadertoy-player.html
  18. async function init() {
  19. const material = new ExtendedShaderMaterial({
  20. uniforms: uniforms,
  21. defines: {
  22. ['IS_SCREEN']: isScreen ? '1' : '0',
  23. ['IS_LINEAR_OUTPUT']: isScreen ? '1' : '0',
  24. },
  25. glslVersion: GLSL3,
  26. vertexShader: toyVert,
  27. fragmentShader: toyFrag,
  28. transparent: true,
  29. depthTest: false,
  30. depthWrite: false,
  31. premultipliedAlpha: false,
  32. }, channels, false)
  33. material.registerMaterialExtensions([toyExtension])
  34. material.needsUpdate = true
  35. const viewer = new ThreeViewer({
  36. canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
  37. msaa: false,
  38. rgbm: false,
  39. tonemap: false,
  40. plugins: [LoadingScreenPlugin, CanvasSnapshotPlugin],
  41. screenShader: material,
  42. renderScale: 2,
  43. })
  44. // setup css alignment of canvas inside container (for proper viewer size)
  45. viewer.container.style.position = 'relative'
  46. viewer.canvas.style.position = 'absolute'
  47. viewer.canvas.style.top = '50%'
  48. viewer.canvas.style.left = '50%'
  49. viewer.canvas.style.transform = 'translate(-50%, -50%)'
  50. addMouseListeners(viewer.canvas)
  51. viewer.addEventListener('preFrame', (ev)=>{
  52. if (!params.running && !params.stepFrame) return
  53. // uniforms.iTimeDelta.value = viewer.renderManager.clock.getDelta()
  54. uniforms.iTimeDelta.value = (ev.deltaTime || 0) / 1000.0
  55. const date = new Date()
  56. uniforms.iDate.value.set(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() * 60 * 60 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds() / 1000)
  57. uniforms.iFrameRate.value = 30 // todo: get from clock
  58. const bufferSize = [viewer.renderManager.renderSize.width * viewer.renderManager.renderScale, viewer.renderManager.renderSize.height * viewer.renderManager.renderScale]
  59. uniforms.iResolution.value.set(bufferSize[0], bufferSize[1], 1)
  60. uniforms.iMouse.value.set( // acc to shadertoy
  61. mouse.position.x * bufferSize[0],
  62. mouse.position.y * bufferSize[1],
  63. mouse.clickPosition.x * (mouse.isDown ? 1 : -1) * bufferSize[0],
  64. mouse.clickPosition.y * (mouse.isClick ? 1 : -1) * bufferSize[1],
  65. )
  66. params.time += uniforms.iTimeDelta.value
  67. uniforms.iTime.value = params.time
  68. uniforms.iFrame.value = params.frame++
  69. // uniforms.iChannelTime.value = [0, 0, 0, 0]
  70. // uniforms.iChannelResolution.value = [
  71. // new Vector3(uniforms.iChannel0Size.value.x, uniforms.iChannel0Size.value.y, 1),
  72. // new Vector3(uniforms.iChannel1Size.value.x, uniforms.iChannel1Size.value.y, 1),
  73. // new Vector3(uniforms.iChannel2Size.value.x, uniforms.iChannel2Size.value.y, 1),
  74. // new Vector3(uniforms.iChannel3Size.value.x, uniforms.iChannel3Size.value.y, 1),
  75. // ]
  76. // for (let i = 0; i < channels.length; i++) {
  77. // const channel = uniforms[channels[i]]
  78. // if (channel.value) {
  79. // channel.value.needsUpdate = true
  80. // uniforms[channels[i] + 'Size'].value.set(channel.value.image.width, channel.value.image.height)
  81. // } else {
  82. // uniforms[channels[i] + 'Size'].value.set(0, 0)
  83. // }
  84. // }
  85. material.uniformsNeedUpdate = true
  86. viewer.setDirty()
  87. ui.uiRefresh?.(true)
  88. params.stepFrame = false
  89. })
  90. viewer.setRenderSize(params.resolution)
  91. const setShader = (v: string)=>{
  92. toyExtension.parsFragmentSnippet = v
  93. toyExtension.computeCacheKey = Math.random().toString()
  94. material.setDirty()
  95. viewer.setDirty()
  96. ui.uiRefresh?.(true)
  97. }
  98. const ui: UiObjectConfig = {
  99. label: 'Edit Properties',
  100. type: 'folder',
  101. expanded: true,
  102. value: params,
  103. children: [{
  104. type: 'vec',
  105. path: 'resolution',
  106. label: 'Resolution',
  107. bounds: [10, 4096],
  108. stepSize: 1,
  109. onChange: ()=>{
  110. viewer.setRenderSize(params.resolution, 'contain', 1)
  111. },
  112. }, {
  113. type: 'number',
  114. path: 'time',
  115. label: 'Time',
  116. readOnly: true,
  117. }, {
  118. type: 'number',
  119. path: 'frame',
  120. label: 'Frame',
  121. readOnly: true,
  122. }, {
  123. type: 'button',
  124. baseWidth: '100%',
  125. label: ()=> 'Step',
  126. disabled: ()=> params.running,
  127. onClick: ()=>{
  128. params.stepFrame = true
  129. ui.uiRefresh?.(true)
  130. },
  131. }, {
  132. type: 'button',
  133. baseWidth: '100%',
  134. label: ()=> params.running ? 'Pause' : 'Play',
  135. onClick: ()=>{
  136. params.running = !params.running
  137. ui.uiRefresh?.(true)
  138. },
  139. }, {
  140. type: 'button',
  141. baseWidth: '100%',
  142. label: ()=> 'Reset',
  143. onClick: ()=>{
  144. params.frame = 0
  145. params.time = 0
  146. params.stepFrame = true
  147. ui.uiRefresh?.(true)
  148. },
  149. }, {
  150. type: 'button',
  151. baseWidth: '100%',
  152. label: ()=> 'Edit Shader',
  153. onClick: ()=>setupShaderEditor(toyExtension.parsFragmentSnippet as string, setShader),
  154. }, {
  155. type: 'button',
  156. label: 'Download png',
  157. baseWidth: '100%',
  158. onClick: async()=>{
  159. const running = params.running
  160. params.running = false
  161. await viewer.getPlugin(CanvasSnapshotPlugin)?.downloadSnapshot('snapshot.png', {
  162. waitForProgressive: false,
  163. displayPixelRatio: undefined,
  164. })
  165. params.running = running
  166. ui.uiRefresh?.(true)
  167. },
  168. }],
  169. }
  170. const shaderFile = 'https://asset-samples.threepipe.org/shaders/tunnel-cylinders.glsl'
  171. const response = await fetch(shaderFile)
  172. const shaderText = await response.text()
  173. setShader(shaderText)
  174. const uiPlugin = viewer.addPluginSync(new TweakpaneUiPlugin(true))
  175. // const uiPlugin = viewer.addPluginSync(new BlueprintJsUiPlugin())
  176. uiPlugin.appendChild(ui)
  177. // uiPlugin.setupPluginUi(CanvasSnapshotPlugin, {expanded: true})
  178. }
  179. // region variables
  180. const params = {
  181. resolution: new Vector2(1280, 720),
  182. time: 0,
  183. frame: 0,
  184. stepFrame: false,
  185. running: true,
  186. }
  187. const mouse = {
  188. position: new Vector2(),
  189. clickPosition: new Vector2(),
  190. isDown: false,
  191. isClick: false,
  192. clientX: 0,
  193. clientY: 0,
  194. }
  195. const isScreen = true
  196. const channels = ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3']
  197. const uniforms = {
  198. iResolution: {value: new Vector3()},
  199. iTime: {value: 0},
  200. iFrame: {value: 0},
  201. iMouse: {value: new Vector4()},
  202. iTimeDelta: {value: 0},
  203. iDate: {value: new Vector4()},
  204. iFrameRate: {value: 0},
  205. iChannel0: {value: null},
  206. iChannel1: {value: null},
  207. iChannel2: {value: null},
  208. iChannel3: {value: null},
  209. iChannel0Size: {value: new Vector2()},
  210. iChannel1Size: {value: new Vector2()},
  211. iChannel2Size: {value: new Vector2()},
  212. iChannel3Size: {value: new Vector2()},
  213. iChannelTime: {value: [0, 0, 0, 0]},
  214. iChannelResolution: {value: [new Vector3(), new Vector3(), new Vector3(), new Vector3()]},
  215. }
  216. // endregion variables
  217. // region shaders
  218. const toyDefault = glsl`
  219. void mainImage( out vec4 fragColor, in vec2 fragCoord )
  220. {
  221. // Normalized pixel coordinates (from 0 to 1)
  222. vec2 uv = fragCoord/iResolution.xy;
  223. fragColor = vec4(uv, 0, 1);
  224. }
  225. `
  226. const toyFrag = glsl`
  227. precision highp int;
  228. precision highp sampler2D;
  229. #define HW_PERFORMANCE 0
  230. uniform vec3 iResolution; // viewport resolution (in pixels)
  231. uniform float iTime; // shader playback time (in seconds)
  232. //uniform float iGlobalTime; // shader playback time (in seconds)
  233. uniform vec4 iMouse; // mouse pixel coords
  234. uniform vec4 iDate; // (year, month, day, time in seconds)
  235. uniform float iSampleRate; // sound sample rate (i.e., 44100)
  236. vec3 iChannelResolution[4]; // channel resolution (in pixels)
  237. //uniform float iChannelTime[4]; // channel playback time (in sec)
  238. //uniform vec2 ifFragCoordOffsetUniform; // used for tiled based hq rendering
  239. uniform float iTimeDelta; // render time (in seconds)
  240. uniform int iFrame; // shader playback frame
  241. uniform float iFrameRate;
  242. uniform vec2 iChannel0Size;
  243. uniform vec2 iChannel1Size;
  244. uniform vec2 iChannel2Size;
  245. uniform vec2 iChannel3Size;
  246. in vec2 vUv;
  247. #define gl_FragColor glFragColor
  248. layout(location = 0) out vec4 glFragColor;
  249. void main() {
  250. iChannelResolution[0] = vec3(iChannel0Size,1.0);
  251. iChannelResolution[1] = vec3(iChannel1Size,1.0);
  252. iChannelResolution[2] = vec3(iChannel2Size,1.0);
  253. iChannelResolution[3] = vec3(iChannel3Size,1.0);
  254. // mainImage(glFragColor,iResolution.xy*vUv); // this has issues in windows?
  255. mainImage(glFragColor,gl_FragCoord.xy);
  256. vec4 diffuseColor = glFragColor;
  257. #glMarker
  258. glFragColor = diffuseColor;
  259. #if IS_SCREEN == 1
  260. glFragColor.a = 1.0;
  261. #ifdef IS_LINEAR_OUTPUT
  262. //glFragColor = sRGBToLinear(glFragColor);
  263. #else
  264. #include <colorspace_fragment>
  265. #endif
  266. #endif
  267. }
  268. `
  269. const toyVert = glsl`
  270. out vec2 vUv;
  271. void main() {
  272. vUv = uv;
  273. gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  274. }
  275. `
  276. // endregion shaders
  277. // region mouse
  278. export function getMouseFromEvent(canvas: HTMLElement, e: PointerEvent|WheelEvent): Vector2 | null {
  279. const rect = canvas.getBoundingClientRect()
  280. const x = e.clientX - rect.left
  281. const y = e.clientY - rect.top
  282. if (x < 0 || y < 0 || x > rect.width || y > rect.height) return null
  283. return mouse.position.set(x / rect.width, 1.0 - y / rect.height)
  284. }
  285. export function onPointerDown(e: PointerEvent, canvas: HTMLElement) {
  286. if (e.button !== 0 || !mouse) return
  287. mouse.isDown = false
  288. mouse.isClick = false
  289. const m = getMouseFromEvent(canvas, e)
  290. if (!m) return
  291. mouse.isDown = true
  292. mouse.isClick = true
  293. mouse.clickPosition.copy(m)
  294. e.preventDefault()
  295. e.stopPropagation()
  296. }
  297. export function onPointerUp(e: PointerEvent, canvas: HTMLElement) {
  298. if (e.button !== 0 || !mouse) return
  299. mouse.isDown = false
  300. mouse.isClick = false
  301. getMouseFromEvent(canvas, e)
  302. }
  303. export function onPointerMove(e: PointerEvent, canvas: HTMLElement) {
  304. if (!mouse) return
  305. mouse.clientX = e.clientX
  306. mouse.clientY = e.clientY
  307. if (!mouse.isDown) return
  308. getMouseFromEvent(canvas, e)
  309. }
  310. export function onPointerWheel(e: WheelEvent, canvas: HTMLElement) {
  311. if (!mouse) return
  312. mouse.clientX = e.clientX
  313. mouse.clientY = e.clientY
  314. const m = getMouseFromEvent(canvas, e)
  315. if (!m) return
  316. mouse.position.set(0, 0)
  317. mouse.clickPosition.set(0, 0)
  318. }
  319. export function addMouseListeners(canvas: HTMLElement) {
  320. canvas.addEventListener('pointerdown', (e) => onPointerDown(e as PointerEvent, canvas), {passive: false})
  321. canvas.addEventListener('pointerup', (e) => onPointerUp(e as PointerEvent, canvas), {passive: false})
  322. canvas.addEventListener('pointermove', (e) => onPointerMove(e as PointerEvent, canvas), {passive: false})
  323. canvas.addEventListener('wheel', (e) => onPointerWheel(e as WheelEvent, canvas), {passive: false})
  324. }
  325. // endregion mouse
  326. // region shader editor
  327. const toyExtension: MaterialExtension = {
  328. parsFragmentSnippet: toyDefault,
  329. isCompatible: () => true,
  330. computeCacheKey: Math.random().toString(),
  331. }
  332. let editor: HTMLElement | undefined = undefined
  333. window.addEventListener('keydown', (e: KeyboardEvent) => {
  334. if (e.key === 'Escape' && editor) {
  335. editor.remove()
  336. editor = undefined
  337. }
  338. })
  339. export function setupShaderEditor(value: string, onChange: (v: string)=>void) {
  340. if (editor) return
  341. editor = document.createElement('div')
  342. editor.classList.add('editor-container')
  343. document.body.appendChild(editor)
  344. const textarea = document.createElement('textarea')
  345. textarea.value = value
  346. textarea.addEventListener('input', ()=>{
  347. onChange(textarea.value)
  348. })
  349. editor.appendChild(textarea)
  350. const closeButton = document.createElement('div')
  351. closeButton.classList.add('close-button')
  352. closeButton.textContent = '×'
  353. closeButton.addEventListener('click', ()=>{
  354. if (!editor) return
  355. editor.remove()
  356. editor = undefined
  357. })
  358. editor.appendChild(closeButton)
  359. }
  360. // endregion shader editor
  361. _testStart()
  362. init().finally(_testFinish)
  363. createStyles(css`
  364. *:focus {
  365. outline: none;
  366. }
  367. .editor-container {
  368. width: min(800px, 80%);
  369. height: min(600px, 80%);
  370. position: absolute;
  371. top: 50%;
  372. left: 50%;
  373. transform: translate(-50%, -50%);
  374. z-index: 1000;
  375. background-color: rgba(0, 0, 0, 0.6);
  376. backdrop-filter: blur(10px);
  377. border-radius: 10px;
  378. overflow: hidden;
  379. }
  380. .editor-container textarea {
  381. height: 100%;
  382. width: 100%;
  383. color: rgba(240, 240, 240, 0.9);
  384. font-family: monospace;
  385. font-size: 14px;
  386. white-space: pre;
  387. overflow: auto;
  388. background: rgba(255, 255, 255, 0.10);
  389. border-radius: 10px;
  390. border: none;
  391. outline: none;
  392. padding: 10px;
  393. backdrop-filter: blur(6px);
  394. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  395. transition: box-shadow 0.2s ease-in-out;
  396. }
  397. .editor-container textarea:hover {
  398. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 2px 0 rgba(255, 255, 255, 0.15) inset;
  399. }
  400. .editor-container .close-button {
  401. position: absolute;
  402. top: 10px;
  403. right: 10px;
  404. z-index: 1;
  405. width: 36px;
  406. height: 36px;
  407. border-radius: 50%;
  408. background: rgba(255, 255, 255, 0.15);
  409. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 1.5px 0 rgba(255, 255, 255, 0.15) inset;
  410. border: none;
  411. color: #fff;
  412. font-size: 20px;
  413. font-weight: bold;
  414. cursor: pointer;
  415. display: flex;
  416. align-items: center;
  417. justify-content: center;
  418. backdrop-filter: blur(4px);
  419. transition: background 0.4s, box-shadow 0.4s;
  420. }
  421. .editor-container .close-button:hover {
  422. background: rgba(255, 255, 255, 0.3);
  423. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  424. }
  425. `)