threepipe
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

script.ts 15KB

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