effect.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. 'use strict';
  2. const { readFileSync, existsSync } = require('fs');
  3. const { updateElementReadonly } = require('../utils/assets');
  4. exports.template = /* html */`
  5. <div class="asset-effect">
  6. <ui-prop>
  7. <ui-label slot="label" value="i18n:ENGINE.assets.effect.shader" tooltip="i18n:ENGINE.assets.effect.shaderTip"></ui-label>
  8. <ui-select slot="content" class="shader-select"></ui-select>
  9. </ui-prop>
  10. <ui-section expand class="section" cache-expand="effect-combinations">
  11. <ui-label slot="header" value="i18n:ENGINE.assets.effect.combinations" tooltip="i18n:ENGINE.assets.effect.combinationsTip"></ui-label>
  12. <div class="description">
  13. <ui-label value="i18n:ENGINE.assets.effect.choose"></ui-label>
  14. </div>
  15. <div class="combinations">
  16. </div>
  17. </ui-section>
  18. <div class="codes"></div>
  19. <ui-label class="multiple-warn-tip" value="i18n:ENGINE.assets.multipleWarning"></ui-label>
  20. </div>
  21. `;
  22. exports.style = /* css */`
  23. .asset-effect {
  24. padding-right: 4px;
  25. }
  26. .asset-effect[multiple-invalid] > *:not(.multiple-warn-tip) {
  27. display: none!important;
  28. }
  29. .asset-effect[multiple-invalid] > .multiple-warn-tip {
  30. display: block;
  31. }
  32. .asset-effect .multiple-warn-tip {
  33. display: none;
  34. text-align: center;
  35. color: var(--color-focus-contrast-weakest);
  36. margin-top: 8px;
  37. }
  38. .asset-effect > .section > .description {
  39. text-align: center;
  40. color: var(--color-normal-fill-weakest);
  41. }
  42. .asset-effect > .section > .combinations .tab {
  43. margin-left: 4px;
  44. min-width: 60px;
  45. width: calc(50% - 4px);
  46. }
  47. .asset-effect > .section > .combinations .tab[checked="true"] {
  48. background-color: var(--color-info-fill-important);
  49. border-color: var(--color-info-fill-important);
  50. color: var(--color-info-contrast-important);
  51. }
  52. .asset-effect > .codes .tabs {
  53. margin: 4px auto 6px auto;
  54. }
  55. .asset-effect > .codes .tabs > .tab {
  56. padding: 0;
  57. width: 110px;
  58. height: 20px;
  59. box-sizing: border-box;
  60. text-align: center;
  61. cursor: pointer;
  62. display: inline-block;
  63. color: var(--color-normal-contrast-emphasis);
  64. border: calc(var(--size-normal-border) * 1px) solid var(--color-default-border);
  65. }
  66. .asset-effect > .codes .tabs > .tab:first-child {
  67. border-top-left-radius: 2px;
  68. border-bottom-left-radius: 2px;
  69. border-right: 1px solid var(--color-default-border);
  70. }
  71. .asset-effect > .codes .tabs > .tab:last-child {
  72. border-top-right-radius: 2px;
  73. border-bottom-right-radius: 2px;
  74. border-left: 1px solid var(--color-default-border);
  75. }
  76. .asset-effect > .codes .tabs > .tab:hover,
  77. .asset-effect > .codes .tabs > .tab[active="true"] {
  78. background-color: var(--color-default-fill-normal);
  79. background-color: var(--color-default-fill-important);
  80. color: var(--color-normal-contrast);
  81. }
  82. .asset-effect > .codes ui-code {
  83. max-height: 400px;
  84. border: none;
  85. border-radius: 0;
  86. background-color: var(--color-normal-fill-emphasis);
  87. }
  88. `;
  89. exports.$ = {
  90. container: '.asset-effect',
  91. shaderSelect: '.shader-select',
  92. combinations: '.combinations',
  93. codes: '.codes',
  94. };
  95. /**
  96. * Property corresponds to the edit element
  97. */
  98. const Elements = {
  99. shaders: {
  100. ready() {
  101. const panel = this;
  102. panel.shadersIndex = 0;
  103. panel.$.shaderSelect.addEventListener('change', (event) => {
  104. panel.shadersIndex = event.target.value;
  105. // There are other properties that are updated depending on its change
  106. Elements.combinations.update.call(panel);
  107. Elements.codes.update.call(panel);
  108. });
  109. panel.$.shaderSelect.addEventListener('confirm', () => {
  110. panel.dispatch('snapshot');
  111. });
  112. },
  113. update() {
  114. const panel = this;
  115. let optionsHtml = '';
  116. panel.shaders.forEach((shader, index) => {
  117. optionsHtml += `<option value="${index}">${shader.name}</option>`;
  118. });
  119. panel.$.shaderSelect.innerHTML = optionsHtml;
  120. if (panel.shadersIndex > panel.shaders.length - 1) {
  121. panel.shadersIndex = 0;
  122. }
  123. panel.$.shaderSelect.value = panel.shadersIndex;
  124. if (panel.shaders[panel.shadersIndex]) {
  125. panel.$.shaderSelect.setAttribute('tooltip', panel.shaders[panel.shadersIndex].name);
  126. }
  127. updateElementReadonly.call(this, panel.$.shaderSelect);
  128. },
  129. },
  130. combinations: {
  131. update() {
  132. const panel = this;
  133. panel.$.combinations.innerText = '';
  134. panel.shaders[panel.shadersIndex].defines.forEach((define) => {
  135. if (!define._enabled) {
  136. return;
  137. }
  138. const prop = document.createElement('ui-prop');
  139. panel.$.combinations.appendChild(prop);
  140. const label = document.createElement('ui-label');
  141. label.setAttribute('slot', 'label');
  142. label.setAttribute('value', define.name);
  143. prop.appendChild(label);
  144. const content = document.createElement('div');
  145. content.setAttribute('slot', 'content');
  146. prop.appendChild(content);
  147. define._values.forEach((value) => {
  148. const userCombinations = panel.combinations[panel.shadersIndex][define.name];
  149. const checked = userCombinations && userCombinations.includes(value) ? 'true' : 'false';
  150. const name = typeof value === 'boolean' ? (value ? 'on' : 'off') : value.toString();
  151. const button = document.createElement('ui-button');
  152. updateElementReadonly.call(panel, button);
  153. button.setAttribute('class', 'tab');
  154. button.setAttribute('checked', checked);
  155. button.innerText = name;
  156. button.addEventListener('click', () => {
  157. if (!panel.combinations[panel.shadersIndex][define.name]) {
  158. panel.combinations[panel.shadersIndex][define.name] = [];
  159. }
  160. const userCombinations = panel.combinations[panel.shadersIndex][define.name];
  161. if (userCombinations.indexOf(value) !== -1) {
  162. // Eliminate existing, can choose more
  163. userCombinations.splice(userCombinations.indexOf(value), 1);
  164. button.setAttribute('checked', 'false');
  165. } else {
  166. userCombinations.push(value);
  167. button.setAttribute('checked', 'true');
  168. }
  169. panel.change();
  170. });
  171. content.appendChild(button);
  172. });
  173. });
  174. if (panel.$.combinations.children.length) {
  175. panel.$.combinations.parentElement.style.display = 'block';
  176. } else {
  177. panel.$.combinations.parentElement.style.display = 'none';
  178. }
  179. },
  180. },
  181. codes: {
  182. ready() {
  183. const panel = this;
  184. panel.glslNames = {
  185. glsl3: 'GLSL 300 ES Output',
  186. glsl1: 'GLSL 100 Output',
  187. };
  188. panel.shaderNames = {
  189. vert: 'Vertex Shader',
  190. frag: 'Fragment Shader',
  191. };
  192. },
  193. update() {
  194. const panel = this;
  195. panel.$.codes.innerText = '';
  196. for (const glslKey in panel.glslNames) {
  197. const section = document.createElement('ui-section');
  198. panel.$.codes.appendChild(section);
  199. section.setAttribute('class', 'section');
  200. section.setAttribute('expand', '');
  201. section.setAttribute('cache-expand', `effect-${glslKey}`);
  202. const glslName = panel.glslNames[glslKey];
  203. const header = document.createElement('div');
  204. section.appendChild(header);
  205. header.setAttribute('slot', 'header');
  206. header.innerHTML = `<span>${glslName}</span>`;
  207. const tabs = document.createElement('div');
  208. section.appendChild(tabs);
  209. tabs.setAttribute('class', 'tabs');
  210. const code = document.createElement('ui-code');
  211. section.appendChild(code);
  212. code.setAttribute('language', 'glsl');
  213. code.innerHTML = panel.shaders[panel.shadersIndex][glslKey][panel.shaders[panel.shadersIndex][glslKey].activeKey];
  214. for (const shaderKey in panel.shaderNames) {
  215. const shaderName = panel.shaderNames[shaderKey];
  216. const active = panel.shaders[panel.shadersIndex][glslKey].activeKey === shaderKey;
  217. const tab = document.createElement('div');
  218. tabs.appendChild(tab);
  219. tab.setAttribute('class', 'tab');
  220. tab.setAttribute('active', active);
  221. tab.innerText = shaderName;
  222. tab.addEventListener('click', () => {
  223. if (tab.getAttribute('active') === 'true') {
  224. return;
  225. }
  226. for (const child of tab.parentElement.children) {
  227. if (child === tab) {
  228. child.setAttribute('active', 'true');
  229. } else {
  230. child.setAttribute('active', 'false');
  231. }
  232. }
  233. panel.shaders[panel.shadersIndex][glslKey].activeKey = shaderKey;
  234. code.innerHTML = panel.shaders[panel.shadersIndex][glslKey][panel.shaders[panel.shadersIndex][glslKey].activeKey];
  235. });
  236. }
  237. }
  238. },
  239. },
  240. };
  241. exports.methods = {
  242. record() {
  243. return JSON.stringify({ shadersIndex: this.shadersIndex });
  244. },
  245. restore(record) {
  246. record = JSON.parse(record);
  247. this.$.shaderSelect.value = record.shadersIndex;
  248. this.$.shaderSelect.dispatch('change');
  249. return true;
  250. },
  251. refresh() {
  252. const panel = this;
  253. // notice : The data displayed uses both the library and the meta, so you need to keep both consistent
  254. if (panel.asset.uuid !== panel.meta.uuid) {
  255. return false;
  256. }
  257. const fileSource = panel.asset.library['.json'];
  258. if (!fileSource || !existsSync(fileSource)) {
  259. console.error('Read effect json file in library failed.');
  260. return false;
  261. }
  262. const dataSource = JSON.parse(readFileSync(fileSource, 'utf8'));
  263. if (!dataSource) {
  264. console.error('Read effect json file in library failed.');
  265. return false;
  266. }
  267. panel.shaders = dataSource.shaders;
  268. // The edited value of defines in each shader
  269. panel.combinations = [];
  270. if (Array.isArray(panel.meta.userData.combinations)) {
  271. panel.combinations = panel.meta.userData.combinations;
  272. }
  273. // Adjusting some data for display
  274. panel.shaders.forEach((shader, index) => {
  275. for (const glslKey in panel.glslNames) {
  276. shader[glslKey].activeKey = 'vert';
  277. }
  278. if (!panel.combinations[index]) {
  279. panel.combinations[index] = {};
  280. }
  281. // Configurable definition with injection of temporary data
  282. shader.defines.forEach((define) => {
  283. const { name, type } = define;
  284. if (name.startsWith('CC_')) {
  285. // Prefixed with "CC_" are not processed
  286. define._enabled = false;
  287. return;
  288. } else {
  289. define._enabled = true;
  290. }
  291. // The following data is used for display editing
  292. define._values = [];
  293. if (type === 'number' && define.range) {
  294. const [min, max] = define.range;
  295. for (let i = min; i <= max; i++) {
  296. define._values.push(i);
  297. }
  298. }
  299. if (type === 'boolean') {
  300. define._values = [false, true];
  301. }
  302. if (type === 'string') {
  303. define._values = define.options;
  304. }
  305. });
  306. });
  307. return true;
  308. },
  309. change() {
  310. const panel = this;
  311. // Need to exclude empty arrays, otherwise scene will report an error
  312. const submitData = [];
  313. panel.combinations.forEach((combination, index) => {
  314. submitData[index] = {};
  315. const names = Object.keys(combination);
  316. names.forEach((name) => {
  317. if (combination[name].length !== 0) {
  318. submitData[index][name] = combination[name];
  319. }
  320. });
  321. });
  322. panel.meta.userData.combinations = submitData;
  323. panel.dispatch('change');
  324. panel.dispatch('snapshot');
  325. },
  326. };
  327. exports.ready = function() {
  328. for (const prop in Elements) {
  329. const element = Elements[prop];
  330. if (element.ready) {
  331. element.ready.call(this);
  332. }
  333. }
  334. };
  335. exports.update = function(assetList, metaList) {
  336. this.assetList = assetList;
  337. this.metaList = metaList;
  338. this.asset = assetList[0];
  339. this.meta = metaList[0];
  340. if (assetList.length > 1) {
  341. this.$.container.setAttribute('multiple-invalid', '');
  342. return;
  343. } else {
  344. this.$.container.removeAttribute('multiple-invalid');
  345. }
  346. const isLegal = this.refresh();
  347. if (!isLegal) {
  348. return;
  349. }
  350. for (const prop in Elements) {
  351. const element = Elements[prop];
  352. if (element.update) {
  353. element.update.call(this);
  354. }
  355. }
  356. };