Compare commits

...

318 Commits

Author SHA1 Message Date
Alexander Rose
a9e0d8236c 4.7.0 2024-09-29 11:12:30 -07:00
Alexander Rose
fc47276fc3 changelog 2024-09-29 11:08:07 -07:00
Huiyu CAI
c60334b97b allow nested components (#1227)
* allow nested components

* header and CHANGELOG edits

* fix header edit

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-09-29 20:05:41 +02:00
Alexander Rose
36d58d0ff0 Merge pull request #1273 from pechersky/sass-color
Move away from deprecated SASS explicit color by using `invert` and `change`
2024-09-29 10:30:40 -07:00
Alexander Rose
73529a890b update mesoscale-explorer style.scss 2024-09-29 10:25:44 -07:00
Yakov Pechersky
b9e88d61a1 master lock 2024-09-29 02:17:22 -04:00
Yakov Pechersky
04bfe71131 Merge branch 'master' into sass-color 2024-09-29 02:10:56 -04:00
Yakov Pechersky
b16c51825a clean up imports 2024-09-29 02:09:44 -04:00
Yakov Pechersky
12630dd9f5 fix lighten-darken -- different in light vs dark/blue 2024-09-29 02:05:43 -04:00
Yakov Pechersky
880b73a3c4 Revert "remove color-lower-contrast"
This reverts commit 12ff3aad93.
2024-09-29 02:00:09 -04:00
Yakov Pechersky
63e7ba57bc Revert "remove color-increase-contrast"
This reverts commit 745d8b80d7.
2024-09-29 01:59:20 -04:00
Alexander Rose
bc2d8a4ce1 schema updates 2024-09-28 19:44:51 -07:00
Alexander Rose
9f951dbeac package updates 2024-09-28 19:18:12 -07:00
Alexander Rose
cba1c23b4d Merge pull request #1277 from molstar/me-color-style-fixes
ME: color and style fixes
2024-09-28 18:39:16 -07:00
Alexander Rose
d63663a2ea changelog 2024-09-28 18:32:58 -07:00
Alexander Rose
41c5ebf1f3 fix entities of root group not updated 2024-09-28 18:31:10 -07:00
Alexander Rose
757cf0cd13 fix shinyDof 2024-09-28 16:49:49 -07:00
Alexander Rose
ad8d07cfaa me: only change style not color in quick-styles ui 2024-09-28 14:51:54 -07:00
Alexander Rose
e0b307d1a8 Merge pull request #1276 from molstar/revert-1275-me_preset
Revert "fix preset coloring in ME"
2024-09-26 23:08:04 -07:00
Alexander Rose
729306f142 Revert "adding 'ent:' break the preset (#1275)"
This reverts commit 8568656d44.
2024-09-26 23:07:54 -07:00
ludovic autin
8568656d44 adding 'ent:' break the preset (#1275) 2024-09-26 12:35:15 +02:00
Yakov Pechersky
b7ba8322d1 CHANGELOG 2024-09-23 15:02:57 -04:00
Yakov Pechersky
818a0dac0d npm update sass 2024-09-23 13:45:54 -04:00
Yakov Pechersky
3f96ba92ce reset lock 2024-09-23 13:45:30 -04:00
Yakov Pechersky
b356f217ab imports where necessary 2024-09-23 13:40:10 -04:00
Yakov Pechersky
a968fb0984 update sass dep 2024-09-23 13:24:13 -04:00
Yakov Pechersky
745d8b80d7 remove color-increase-contrast
use `color.adjust` instead
2024-09-23 13:20:50 -04:00
Yakov Pechersky
12ff3aad93 remove color-lower-contrast
use `color.adjust` instead
2024-09-23 13:20:02 -04:00
Alexander Rose
e8501b73a5 illumination params description 2024-09-23 09:51:50 -07:00
Yakov Pechersky
9c07da6de6 also set alpha using api 2024-09-23 12:35:53 -04:00
Yakov Pechersky
8c2e58b67c Move away from deprecate SASS explicit color by using invert
In building, saw
```
Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.

More info: https://sass-lang.com/d/legacy-js-api
```
Instead of using `color.channel(..., "red")`, we really were deconstructing rgb by channel just to invert.
So use the underlying SASS api directly.
2024-09-23 12:30:51 -04:00
Alexander Rose
80d7649dbb illumination params description 2024-09-22 19:29:43 -07:00
David Sehnal
6e63bb4283 ErrorContext util & usage in MVS (#1254)
* error context util & usage in MVS

* changelog
2024-09-22 10:12:00 +02:00
Alexander Rose
ba7a4137fe changelog 2024-09-21 14:56:41 -07:00
Alexander Rose
2ca0a4291b Merge pull request #1269 from molstar/resolution-mode
Resolution mode
2024-09-21 14:54:24 -07:00
Alexander Rose
32a1a35a96 remove debug code 2024-09-21 10:36:45 -07:00
Alexander Rose
df129d8ce3 Merge branch 'master' of https://github.com/molstar/molstar into resolution-mode 2024-09-21 10:24:03 -07:00
Alexander Rose
c346da9f6d Merge pull request #1253 from molstar/illumination
Illumination mode
2024-09-21 10:23:38 -07:00
Alexander Rose
de15a3d05d add resolutionMode parameter to Canvas3DContext 2024-09-21 10:22:57 -07:00
Alexander Rose
392b42f6f0 fix impostor sphere interior normal when using orthographic projection 2024-09-21 10:10:28 -07:00
Alexander Rose
46a9b587b4 handle 0 denoise threshold 2024-09-21 10:08:30 -07:00
Alexander Rose
3334a636c5 Merge branch 'master' of https://github.com/molstar/molstar into illumination 2024-09-15 14:48:26 -07:00
Alexander Rose
94d52dddda fix me color update 2024-09-15 14:48:10 -07:00
Alexander Rose
b35a73b50f tweak params 2024-09-15 14:01:33 -07:00
Alexander Rose
7fde6a810d Merge branch 'master' of https://github.com/molstar/molstar into illumination 2024-09-15 13:42:45 -07:00
Alexander Rose
843eae1e49 Merge pull request #1262 from molstar/shadow-multiple-lights
fix shadows with multiple lights
2024-09-15 13:41:29 -07:00
Alexander Rose
8513183684 Merge branch 'master' of https://github.com/molstar/molstar into illumination 2024-09-14 14:35:41 -07:00
Alexander Rose
790bebf302 normalize direct light for shadows 2024-09-14 14:24:30 -07:00
Alexander Rose
0fb76261e8 spec & avoid compiling tracing shaders when unsupported 2024-09-14 13:58:51 -07:00
Alexander Rose
53c69640b7 spec 2024-09-14 13:46:36 -07:00
Alexander Rose
d70cef8ad3 fix shadows with multiple lights 2024-09-14 13:42:47 -07:00
Alexander Rose
a84a23cbcc remove console.log 2024-09-14 13:27:46 -07:00
Alexander Rose
736f2dc657 ssao tweaks
- linear filter always supported for uint8
- don't use mapped depth in non-multiscale to avoid issues on some mobile devices
2024-09-14 13:17:49 -07:00
Alexander Rose
06295fd586 add shader precision formats info to webgl context 2024-09-14 13:16:00 -07:00
Alexander Rose
e9f4d95dc3 Merge pull request #1260 from molstar/xtc-parser-fix
fix no-compression xtc parser
2024-09-14 13:14:33 -07:00
Alexander Rose
223e3b6fbf Merge branch 'master' into xtc-parser-fix 2024-09-14 13:14:26 -07:00
Alexander Rose
16c967b674 changelog 2024-09-14 13:14:01 -07:00
Alexander Rose
a6a1f0621e improve viewport-screenshot tracing
- make sample count independent of performance for reproducible quality
2024-09-14 13:11:49 -07:00
Alexander Rose
60cb722343 avoid compiling tracing shaders when unsupported 2024-09-14 13:08:54 -07:00
Alexander Rose
2569fe9577 improve direct light shadows 2024-09-14 13:07:25 -07:00
Alexander Rose
be717133ef fix tracing on mobile
- specify precision highp int
2024-09-14 13:06:06 -07:00
Alexander Rose
231d585236 always wait for gpu in canvas3d.render 2024-09-14 13:04:53 -07:00
etongfu
098faf129c Fix mol2 status_bit read error (#1251)
* fix: Fix mol2 status_bit read error

* feat: Retrieve the number of columns using `streaming tokenizer` and remove readLineElements

* fix: remove useless code

* fix: fix status_bits array read error

* chore: remove dataLines code.

* chore: Add contributor information

---------

Co-authored-by: tongfu.e <tongfu.e@xtalpi.com>
2024-09-11 18:58:14 +02:00
dsehnal
0b39ad8341 fix no-compression xtc parser 2024-09-10 19:00:30 +02:00
Alexander Rose
c0117c41e6 Merge branch 'master' of https://github.com/molstar/molstar into illumination 2024-09-08 19:32:30 -07:00
Alexander Rose
0ce41e989a Merge pull request #1255 from giagitom/enable-depth-xray
Enable dXrayShaded define when using depth variant
2024-09-08 16:58:29 -07:00
Alexander Rose
b6885a0d76 improve parameter adjustment 2024-09-07 23:07:42 -07:00
Alexander Rose
125120fcab avoid rendering illumination iteration when previous gpu are not complete 2024-09-07 21:39:02 -07:00
Alexander Rose
2147a5c3fb add automatically adjusted denoise threshold 2024-09-07 11:41:09 -07:00
Alexander Rose
8c7d5b9585 remove extra timer.mark 2024-09-07 11:33:33 -07:00
Alexander Rose
aa4c36885d make userInteractionReleaseMs a param 2024-09-07 11:31:23 -07:00
Alexander Rose
4ee4788378 make "glow" from overly lit bounce rays optional 2024-09-07 11:28:57 -07:00
Alexander Rose
47aea2b12f pause canvas3d while rendering screenshot 2024-09-07 11:23:41 -07:00
Alexander Rose
490bc82ee6 properly wait for gpu to finish when rendering image 2024-09-07 11:22:21 -07:00
Alexander Rose
0d24c636a3 improve webgl timer, support optional note 2024-09-07 11:21:26 -07:00
Alexander Rose
5a81b4f375 add illumination option to viewer app 2024-09-07 11:09:59 -07:00
Alexander Rose
73b90ffb5c fix normal for approximate sphere 2024-09-07 11:09:22 -07:00
giagitom
02e795b265 Update headers 2024-09-05 01:46:31 +02:00
giagitom
325aa74331 changelog 2024-09-05 01:45:22 +02:00
giagitom
1efe2eb329 Enable dXrayShaded define when using depth variant 2024-09-05 01:37:13 +02:00
dsehnal
1e895f3c8c only render first iteration of illumination when actively interacting 2024-09-03 18:18:23 +02:00
dsehnal
028c283043 undo image pass render tweak 2024-09-03 18:06:41 +02:00
dsehnal
144ed51100 fallback to standard rendering when interacting with the structure 2024-09-03 17:16:56 +02:00
Alexander Rose
e3c2ec4561 fix image.frag 2024-09-02 22:48:34 -07:00
Alexander Rose
84dd957983 temp 2024-09-02 22:38:18 -07:00
Alexander Rose
1093a4f6ad lint 2024-09-02 22:37:13 -07:00
Alexander Rose
c4fdc43aa0 changelog 2024-09-02 14:32:25 -07:00
Alexander Rose
15da722af5 add illumination pass 2024-09-02 14:24:27 -07:00
Alexander Rose
eec2d2a720 add tracing pass 2024-09-02 14:23:09 -07:00
Alexander Rose
375db11e9b add visual density parameter to estimate object thickness 2024-08-30 13:56:56 -07:00
Alexander Rose
b1b1972684 add bloom to simple-settings ui 2024-08-29 18:54:47 -07:00
Alexander Rose
ce0d4cbc4e fix renderer.clearDepth 2024-08-29 18:52:39 -07:00
Alexander Rose
127d9bc94e me: increase ultra graphics mode thresholds 2024-08-29 18:48:20 -07:00
Alexander Rose
860df1a898 Merge pull request #1249 from molstar/fix-1245
Fix handling of PDB files
2024-08-29 18:44:33 -07:00
Alexander Rose
51b36e90f0 Merge branch 'master' into fix-1245 2024-08-29 18:44:24 -07:00
Alexander Rose
48b19e149b Merge pull request #1250 from JonStargaryen/master
Sequence Panel: Improve visuals of unmodeled sequence positions
2024-08-29 14:05:08 -07:00
Sebastian Bittrich
5a87d9dbf5 Seq Panel: adjust font color of missing positions (closes #1248) 2024-08-29 13:38:54 -07:00
Sebastian Bittrich
c07b4ba550 Seq Panel: default cursor for missing positions 2024-08-29 13:11:53 -07:00
Alexander Rose
8a99e3e3fd load no default tour 2024-08-29 10:31:06 -07:00
Alexander Rose
571f54f4e6 lint 2024-08-29 10:28:19 -07:00
Alexander Rose
15cd7b9c13 Fix handling of PDB files
- that have chains with same id separated by TER record
2024-08-29 10:22:16 -07:00
Alexander Rose
0d21b399b5 4.6.0 2024-08-28 16:26:11 -07:00
Alexander Rose
94ad0bf75c changelog 2024-08-28 16:22:54 -07:00
Alexander Rose
2c44286ca5 schema update 2024-08-28 16:20:33 -07:00
Alexander Rose
23705727ac package updates 2024-08-28 16:19:44 -07:00
Alexander Rose
0a173d230c Merge pull request #1242 from molstar/library-docs
Using esbuild documentation
2024-08-28 15:01:27 -07:00
Alexander Rose
f8987af0e8 Merge pull request #1247 from corredD/revision_p
PR for Revision
2024-08-28 14:58:17 -07:00
Alexander Rose
e046b80bf2 use now from util 2024-08-28 14:57:34 -07:00
Ludovic Autin
f8d6f1d010 timer for loadURL 2024-08-28 12:15:50 -07:00
ludovic autin
579190b9ce embedding.html page example 2024-08-28 11:09:42 -07:00
dsehnal
e44e29eb9f using esbuild docs 2024-08-27 11:35:10 +02:00
ludovic autin
589cec24e5 more timer. For some reason it doesnt report anything with MG model 2024-08-26 16:46:37 -07:00
ludovic autin
fd999953f9 default MOL loading if no url model given
timingmode for loading mesoscale model ( in loadURL function )
2024-08-26 16:34:16 -07:00
Alexander Rose
523dfe7928 Merge branch 'master' of https://github.com/molstar/molstar 2024-08-24 15:01:12 -07:00
Alexander Rose
b2f26e6b1d fix timer: no resolve when capturing stats 2024-08-24 14:59:39 -07:00
Alexander Rose
dc45bf3915 optimize camera.getPixelSize 2024-08-24 14:59:30 -07:00
Sebastian Bittrich
96e22e25cf ModelServer & VolumeServer: add health-check (#1233)
* VolumeServer: add health-check

* ModelServer: add health-check
2024-08-23 12:49:17 +02:00
Alexander Rose
051beb3c3c Merge pull request #1230 from molstar/compression-api
compression-api
2024-08-19 22:38:05 -07:00
Alexander Rose
2ba3d67520 Merge branch 'master' into compression-api 2024-08-19 22:37:57 -07:00
Alexander Rose
cd30d9c1a3 Merge pull request #1231 from molstar/cg-improvements
improve coarse-grained models handling
2024-08-19 22:37:02 -07:00
Alexander Rose
7d32aa8276 Merge pull request #1224 from molstar/screenshot-task
wrap screenshot & image generation in a Task
2024-08-19 22:36:11 -07:00
Alexander Rose
f837b46da1 Merge branch 'master' into screenshot-task 2024-08-19 22:36:03 -07:00
Alexander Rose
c6107ff694 fix hasCompressionStreamSupport 2024-08-19 22:31:36 -07:00
Alexander Rose
2e7228f88b improve coarse-grained models handling
- Add Zhang-Skolnick secondary-structure assignment method which handles coarse-grained models (#49)
- Calculate bonds for coarse-grained models
2024-08-18 13:44:57 -07:00
Alexander Rose
e8825eac5d fix cartoon representation not updated when secondary structure changes 2024-08-18 13:36:22 -07:00
Alexander Rose
1a88126af8 add pdbx_structure_determination_methodology mmcif field and Model helpers 2024-08-18 13:32:57 -07:00
Alexander Rose
c4a6eba448 compression-api 2024-08-17 19:20:48 -07:00
Alexander Rose
fc7e9501b2 add more coloring options to cartoon theme 2024-08-17 16:35:25 -07:00
Alexander Rose
1dfd52db43 add formal-charge color theme 2024-08-17 16:35:07 -07:00
Alexander Rose
5510b28656 fix polymer-gap visual coloring with cartoon theme 2024-08-17 16:34:26 -07:00
Alexander Rose
e94abdb159 Merge pull request #1120 from giagitom/tubular-alpha-helices-round-cap
Add round caps on tubular alpha helices
2024-08-17 16:07:21 -07:00
Alexander Rose
7015607244 tweaks 2024-08-17 16:05:44 -07:00
Alexander Rose
7ff37d7dcc Merge pull request #1228 from JonStargaryen/master
Support for AlphaFold DB BinaryCIF files and 4-character PDB-Dev IDs
2024-08-17 15:52:59 -07:00
Sebastian Bittrich
3abc2da106 cl 2024-08-16 14:32:50 -07:00
Sebastian Bittrich
f9c498177a PDB-Dev: support 4-char PDB ID 2024-08-16 14:31:37 -07:00
Sebastian Bittrich
872c6483be AFDB: bcif support 2024-08-16 13:55:10 -07:00
Sebastian Bittrich
53288e4e9d typo 2024-08-16 13:27:59 -07:00
giagitom
d6b045594c Round cap fixes 2024-08-14 15:04:29 +02:00
giagitom
aa86111de7 Merge branch 'master' of https://github.com/molstar/molstar into tubular-alpha-helices-round-cap 2024-08-12 10:18:27 +02:00
Alexander Rose
040473388e wrap screenshot & image generation in a Task 2024-08-10 15:38:44 -07:00
Alexander Rose
f474615729 typo 2024-08-10 15:37:06 -07:00
Alexander Rose
92559e456e passes refactor 2024-08-10 15:35:44 -07:00
Alexander Rose
b2434ea0d0 Merge pull request #1220 from molstar/bond-visibility
Avoid calculating bonds for water units when `ignoreHydrogens` is on
2024-08-10 12:38:27 -07:00
Alexander Rose
6cf0ce5574 Merge branch 'master' into bond-visibility 2024-08-10 12:38:20 -07:00
Alexander Rose
518a40f0ba defer sphere3d cloning 2024-08-10 12:36:42 -07:00
Alexander Rose
387e87bfda Merge pull request #1222 from molstar/entity-id-multi-model
Improve entity-id coloring for structures with multiple models from the same source
2024-08-10 12:03:44 -07:00
Alexander Rose
4fac2a5cd6 fix changelog dates 2024-08-10 12:01:42 -07:00
Alexander Rose
f5f3ea84d4 Improve entity-id coloring for structures with multiple models from the same source 2024-08-03 12:51:45 -07:00
Alexander Rose
4b3d470dde Avoid calculating bonds for water units when ignoreHydrogens is on
- Add `Water` trait to `Unit`
- Remove unsued code from `Structure.ofModel`
- Try reuse boundary in element-cross visual
2024-08-03 12:00:27 -07:00
Alexander Rose
8513a44e8c Improved prmtop format support (CTITLE, %COMMENT) 2024-08-03 11:53:22 -07:00
Alexander Rose
84b54d97df Fix missing Sequence UI update on state object removal 2024-08-03 11:49:09 -07:00
Alexander Rose
34606f258e tweak objectForEach type 2024-08-03 11:47:08 -07:00
Alexander Rose
2c10dd46a0 4.5.0 2024-07-28 17:34:12 -07:00
Alexander Rose
d4c80fc995 changelog 2024-07-28 17:30:50 -07:00
Alexander Rose
e1c00f65a5 package updates 2024-07-28 17:29:36 -07:00
Alexander Rose
012bc9e8e8 Merge pull request #1209 from molstar/fix-traj-color-update
ensure color-theme gets correctly updated
2024-07-28 17:27:00 -07:00
Alexander Rose
a99083107c Merge pull request #1207 from giagitom/separed-postprocessing-passes
Separed postprocessing passes
2024-07-28 17:23:15 -07:00
Alexander Rose
7e93bb0dda update filter in schema 2024-07-28 17:20:44 -07:00
giagitom
9735cce043 Merge branch 'master' of https://github.com/molstar/molstar into separed-postprocessing-passes 2024-07-28 12:53:06 +02:00
Alexander Rose
78e1d76f5e Merge branch 'master' into fix-traj-color-update 2024-07-25 22:14:54 -07:00
Alexander Rose
18b1492d54 schema update
- force missing auth_seq_id fields to be int
2024-07-25 22:13:10 -07:00
Alexander Rose
6116b2fea5 Merge pull request #1211 from molstar/ssao-filter
use texture linear filter for SSAO when available
2024-07-25 22:11:10 -07:00
Alexander Rose
6ef8fd2b64 handle missing theme updates
- Fix trajectory-index color-theme not always updated (#896)
 - Fix bond cylinders not updated on size-theme change with `adjustCylinderLength` enabled (#1215)
2024-07-25 22:08:39 -07:00
giagitom
9319805d36 Fix textue size not updating correctly 2024-07-24 11:58:21 +02:00
Alexander Rose
5027ad37d7 use texture linear filter for SSAO when available 2024-07-22 20:55:02 -07:00
Alexander Rose
70bd0c25c4 schema update
- add struct_mon_prot_cis mmcif category
2024-07-22 20:52:50 -07:00
Alexander Rose
1a5c7f5437 cleanup 2024-07-21 14:18:35 -07:00
Alexander Rose
4a9505c334 Merge branch 'master' of https://github.com/molstar/molstar into fix-traj-color-update 2024-07-20 13:40:41 -07:00
Alexander Rose
b43ec9ed45 Merge branch 'master' of https://github.com/molstar/molstar 2024-07-20 13:37:46 -07:00
Alexander Rose
eb9c6d542b package updates 2024-07-20 13:37:44 -07:00
Alexander Rose
2ec0911821 Merge pull request #1201 from rjdirisio/further-sass-syntax-fixes
Continue fixing syntax for Sass deprecation of special semantics
2024-07-20 13:31:28 -07:00
Alexander Rose
bbb34c8a27 ensure color-theme gets correctly updated
- fixes trajectory-index color-theme (#896)
2024-07-20 13:30:26 -07:00
giagitom
1bcb8d6486 Merge remote-tracking branch 'upstream/master' into separed-postprocessing-passes 2024-07-17 16:29:31 +02:00
giagitom
630b5ca203 Separated postprocessing passes 2024-07-17 16:16:53 +02:00
Alexander Rose
5b2ed784e1 Merge pull request #1204 from corredD/patch-1
Update states.tsx docs url
2024-07-16 21:02:18 -07:00
Alexander Rose
11c9a83ee7 Merge pull request #1205 from corredD/ME_fix_loadUrl
Load URL now behave like drag/drop.
2024-07-16 21:00:39 -07:00
ludovic autin
2d1b61647a Load URL now behave like drag/drop. Fix some error loading state snapshot tours 2024-07-16 11:46:25 -07:00
ludovic autin
fa3797a738 Update states.tsx docs url 2024-07-16 10:40:43 -07:00
Alexander Rose
fc60c0c980 Merge pull request #1191 from bergwerf/aromatic_links
Add unadjusted position for aromatic link dashes
2024-07-14 10:42:34 -07:00
Alexander Rose
372ca20980 simplify render-structure-grid test 2024-07-14 10:35:35 -07:00
Alexander Rose
b0aad9f1ff add missing adjust arg 2024-07-14 10:15:35 -07:00
Alexander Rose
40e45adbb0 Merge branch 'master' of https://github.com/molstar/molstar into pr/bergwerf/1191 2024-07-14 10:05:40 -07:00
Alexander Rose
5b43a2cee9 me ui header tweaks 2024-07-14 10:03:10 -07:00
Alexander Rose
a319a0daa8 update me deploy path 2024-07-13 18:44:19 -07:00
Alexander Rose
79f812d0e1 update me deploy path 2024-07-13 18:01:34 -07:00
Ryan DiRisio
45611a25a5 Continue fixing syntax 2024-07-12 12:25:57 -04:00
Ryan DiRisio
77be659915 Update misc.scss msp-no-webgl syntax to be in line with latest sass requirements (#1199)
* wrap args in ampersand for msp-no-webgl

* update changelog

---------

Co-authored-by: Ryan DiRisio <rdirisio@treeline.bio>
2024-07-12 16:54:40 +02:00
Herman Bergwerf
5986250ed9 Make Trackball spin in radians per second (#1193)
* Make Trackball spin in radians per second.

* Add line to changelog.
2024-07-08 18:03:02 +02:00
Herman Bergwerf
8156c672b0 Fix formatting. 2024-07-08 15:59:59 +02:00
Herman Bergwerf
a443512102 Merge branch 'master' into aromatic_links 2024-07-08 15:52:14 +02:00
Herman Bergwerf
4b921319a8 Improve pinch event (#1192)
* Improve pinch event.

* Apply feedback.

* Update changelog and headers.

* Add me to contributers.
2024-07-08 15:39:00 +02:00
Herman Bergwerf
6329820a87 Add line to changelog. 2024-07-08 15:14:26 +02:00
Herman Bergwerf
91e4b0c3d6 Add me to file headers. 2024-07-08 15:11:53 +02:00
Herman Bergwerf
7666617857 Alter LinkBuilderProps.position signature. 2024-07-08 15:10:04 +02:00
Herman Bergwerf
19be1090b3 Add unadjusted position for aromatic link dashes. 2024-07-07 16:39:38 +02:00
Alexander Rose
354438052e Merge pull request #1176 from papillot/hbonds_with_explicit_hydrogens
Take into account explicit hydrogens when creating and representing Hydrogen bonds (regular and weak)
2024-07-06 12:03:05 -07:00
Alexander Rose
b72444b213 Merge branch 'master' of https://github.com/molstar/molstar into pr/papillot/1176 2024-07-06 11:55:51 -07:00
Alexander Rose
179078f45c remove unused postprocessing code (#1189) 2024-07-06 11:48:53 -07:00
Alexander Rose
1dbc23fe91 dof fixes
- handle pixel ratios =! 1
- ensure transparent depths is available
2024-07-06 11:29:31 -07:00
Paul Pillot
ff4dec9fea code styling, typos 2024-07-01 09:43:10 +02:00
Paul Pillot
6ea51c07b4 update changelog, author information 2024-07-01 09:32:11 +02:00
Paul Pillot
f4cebb9195 manage weak hydrogen bonds position
Previous code was filtering on `polar` hydrogens when looking for an anchorage.
This was not necessary as an acceptor for a regular HBond has only polar hydrogens.
It was removed to make the same code compliant with weak C-H bonds.
2024-07-01 09:27:46 +02:00
Paul Pillot
cbe5f0dc7c Add ignoreHydrogens parameter to geometry options
This is different from the "Hydrogens: Show All/Hide All/Only Polar" as it is used for filtering out which contacts are represented, while the latter is used to control the display (point to the heavy atoms when H are hidden).
In other words, by considering the H atoms in the geometric constraints, some candidate H bonds are discarded (e.g. because the donor does not have H atoms in the right orientation), while the visibility of hydrogens parameters will still show the same number of hydrogen bonds, just not pointing to the same atoms.
2024-07-01 09:27:46 +02:00
Paul Pillot
0ac8b565b5 Use explicit hydrogens to calculate angles
Donor: at least one hydrogen on the donor must be within the donor angle deviation towards the acceptor (angle H-X-A < 45º)

Acceptor: use the closest H on the donor to compute the angles. Ensure that every H atom on the acceptor complies with the ideal angle (no bonded atom from acceptor should be in the way).
2024-07-01 09:27:46 +02:00
Paul Pillot
4f38d4d943 relocate hbond on hydrogens from donors only
This avoids hbonds between 2 hydrogen atoms
2024-07-01 09:27:46 +02:00
Alexander Rose
0af84eb6b5 4.4.1 2024-06-30 15:53:58 -07:00
Alexander Rose
aa2d19478b changelog 2024-06-30 15:51:28 -07:00
Alexander Rose
e035b834a6 update package-lock 2024-06-30 15:43:34 -07:00
Alexander Rose
2b2dfd9245 4.4.0 2024-06-30 15:35:36 -07:00
Alexander Rose
678790efa3 changelog 2024-06-30 15:32:59 -07:00
Alexander Rose
a121c5e2cd fix missing unit.kind check 2024-06-30 13:31:09 -07:00
Alexander Rose
47b242244e schema updates 2024-06-30 13:25:30 -07:00
Alexander Rose
2b9d3fd33a packge updates 2024-06-30 13:25:18 -07:00
Alexander Rose
81404036a2 Merge pull request #1182 from corredD/driver_tour
Mesoscale Explorer Driver tour
2024-06-30 13:15:40 -07:00
Alexander Rose
e2dc15cf0f cleanup, fixes & changelog 2024-06-30 13:14:58 -07:00
Ludovic Autin
365a91879f clean up 2024-06-30 10:10:06 -07:00
Ludovic Autin
e47e0eb51a Merge branch 'master' into driver_tour 2024-06-30 10:02:31 -07:00
Alexander Rose
390046e38f Merge pull request #1184 from JonStargaryen/master
Remove support for density data from edmaps.rcsb.org
2024-06-29 22:27:01 -07:00
Sebastian Bittrich
b8eb5191a2 rm support for edmaps.rcsb.org, closes #1127 2024-06-28 11:16:53 -07:00
ludovic autin
4cc416ca28 driver is in the extra folder of molstar.github.io 2024-06-27 11:38:30 -07:00
ludovic autin
f4b2458390 Merge branch 'master' into driver_tour 2024-06-27 10:02:39 -07:00
David Sehnal
1cad6eef74 doNotDisposeCanvas3DContext option in PluginContext.dispose (#1180)
* doNotDisposeCanvas3DContext option in PluginContext

* header
2024-06-27 15:45:05 +02:00
Alexander Rose
2923be6006 Merge pull request #1175 from corredD/list_tours
file holdng the molx available tour.
2024-06-26 21:26:14 -07:00
ludovic autin
70959641a1 Merge branch 'master' into driver_tour
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
#	src/apps/mesoscale-explorer/ui/states.tsx
2024-06-26 10:35:43 -07:00
ludovic autin
051608f56c file holdng the molx available tour. 2024-06-24 13:13:32 -07:00
Alexander Rose
71c1a4e85b Merge pull request #1167 from corredD/ME_quick_style
Mesoscale Explorer Quick Style
2024-06-23 22:30:13 -07:00
Alexander Rose
ba06c9e413 changelog 2024-06-23 22:29:23 -07:00
Ludovic Autin
8e4dfd1ffd default collapsed for structure measurement like the quickstyle. 2024-06-23 17:59:53 -07:00
Ludovic Autin
fac8aa529f spelling + shiny style 2024-06-23 17:51:57 -07:00
Ludovic Autin
d35b4b5e62 Merge branch 'master' into ME_quick_style 2024-06-23 17:13:38 -07:00
Alexander Rose
dd1789478b Merge pull request #1161 from corredD/me_snapshot_descrption
Mesoscale Explorer Snapshot Description
2024-06-23 17:01:33 -07:00
Alexander Rose
af27a00a01 changelog 2024-06-23 17:00:55 -07:00
Ludovic Autin
17cea8f99c not sure why this changes was not in the merge. 2024-06-23 16:14:14 -07:00
Ludovic Autin
4f6d5a7dc7 Merge branch 'master' into me_snapshot_descrption
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
use custom snapshotDescription
2024-06-23 16:08:23 -07:00
ludovic autin
857972653e Merge branch 'molstar:master' into ME_quick_style 2024-06-23 15:47:32 -07:00
Alexander Rose
98ff0f5c55 Merge pull request #1169 from molstar/customize-snapshot-desc
Ability to customize snapshot description component
2024-06-23 15:20:13 -07:00
Alexander Rose
43803a91ea Merge pull request #1170 from molstar/cel-shading-artifacts
cel-shading artifacts
2024-06-23 15:19:13 -07:00
Alexander Rose
8a1bab8bcb Merge pull request #1171 from giagitom/clean_solidInterior_transparent_cylinders
Clean solid interior transparent cylinders
2024-06-23 15:18:55 -07:00
Ludovic Autin
17a47faaff Merge branch 'master' into ME_quick_style2
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-23 15:10:14 -07:00
Ludovic Autin
f793167e91 cleanup, fix cel-shading spelling 2024-06-23 15:06:16 -07:00
giagitom
d1c2c8e837 Updated headers and changelog 2024-06-23 23:42:27 +02:00
giagitom
5d7ef8196e Merge branch 'master' of https://github.com/molstar/molstar into clean_solidInterior_transparent_cylinders 2024-06-23 23:39:21 +02:00
giagitom
e0715cbf5c clean solidInterior transparent cylinders 2024-06-23 23:33:11 +02:00
Alexander Rose
1af8522de3 cel-shading artifacts
- clamp metalness & roughness to look better
2024-06-23 14:30:53 -07:00
Alexander Rose
c6becd5741 Merge pull request #1166 from corredD/ME_select_behavior
Mesoscale Explorer mouse select behavior
2024-06-23 14:06:58 -07:00
Alexander Rose
0ca368f29f changelog 2024-06-23 14:06:45 -07:00
Alexander Rose
5039a448ad cleanup 2024-06-23 13:44:23 -07:00
Ludovic Autin
9ef38f02c9 code fix, and make sure style stay when changing color scheme in the tree. 2024-06-23 10:41:00 -07:00
dsehnal
66f4ff1140 fix types 2024-06-23 18:48:25 +02:00
dsehnal
cc077656a9 ability to customize snapshot description component 2024-06-23 18:30:31 +02:00
Ludovic Autin
3ef1a2ec0a remove gamma, and use exposure instead. Expose renderer/trackball advance settings in left panel.
only expose couple of quick-style.
2024-06-22 13:04:31 -07:00
ludovic autin
144bf6954e apply gamma only if > 1 2024-06-21 11:39:40 -07:00
ludovic autin
50e5538148 - Fixed shadow fallof
- Add Gamma effect
- Quick Style UI default / illustrative cell shading / DOF plastic
2024-06-21 11:32:13 -07:00
ludovic autin
efe95f92c7 also added the StructureMeasurementsControls in the right panel below the selection. 2024-06-21 10:16:06 -07:00
ludovic autin
09f858a755 mouse left click on label with snapsot key load the snapshot. mouse hover label with protein name highlight entities with the same name 2024-06-21 09:48:51 -07:00
ludovic autin
e7082d4ccc focusinfo and other ID 2024-06-19 10:44:19 -07:00
ludovic autin
732a8f4bd0 Merge branch 'master' into driver_tour
# Conflicts:
#	src/apps/mesoscale-explorer/ui/entities.tsx
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-19 10:00:32 -07:00
ludovic autin
82ca06b29e use MesoMarkdownAnchor for the focusInfo 2024-06-19 09:26:01 -07:00
ludovic autin
a05429f13f Merge branch 'master' into me_snapshot_descrption
# Conflicts:
#	src/apps/mesoscale-explorer/data/state.ts
#	src/apps/mesoscale-explorer/ui/entities.tsx
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-19 09:21:29 -07:00
Alexander Rose
377de7ad40 Merge pull request #1118 from giagitom/quantized-light
Add quantized lighting option
2024-06-18 21:50:48 -07:00
Alexander Rose
74f4d00c8d changelog 2024-06-18 21:50:21 -07:00
Alexander Rose
be3825372e lint fix 2024-06-18 21:47:12 -07:00
Alexander Rose
d62c5c9050 Merge pull request #1157 from corredD/me_selection_description
Mesoscale explorer entities selection description
2024-06-18 21:45:21 -07:00
ludovic autin
9ba5112beb revert base css class, create the custom css class in me app style.css .msp-snapshot-description-me 2024-06-17 10:15:16 -07:00
ludovic autin
048658ee39 min,max text size 2024-06-17 09:50:42 -07:00
ludovic autin
2918081dd9 Merge branch 'master' into me_selection_description 2024-06-17 09:43:03 -07:00
ludovic autin
1a67868c07 Merge branch 'master' into me_snapshot_descrption 2024-06-17 09:42:35 -07:00
Alexander Rose
525dfaddd2 add cel-shading
- celShaded geometry parameter
- celSteps renderer parameter
2024-06-16 12:45:06 -07:00
Alexander Rose
8aa12c0d31 Merge branch 'master' of https://github.com/molstar/molstar into pr/giagitom/1118 2024-06-15 14:25:47 -07:00
Alexander Rose
8cb464a686 Merge pull request #1159 from molstar/plddt-bfactor
support reading score from B-factor in pLDDT color theme
2024-06-15 14:24:09 -07:00
Alexander Rose
e0371d7e32 Update src/extensions/model-archive/quality-assessment/color/plddt.ts
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 14:22:43 -07:00
Ludovic Autin
e7da6bc194 duplicate preventDefault, rename function 2024-06-15 10:32:14 -07:00
ludovic autin
933869b5e1 Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:21:19 -07:00
Ludovic Autin
5d6adc46fe simplify get info(). fix import 2024-06-15 10:08:00 -07:00
ludovic autin
b8a98efcaf Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:58 -07:00
ludovic autin
53b358f70a Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:34 -07:00
ludovic autin
bce9e5b0ad Update src/apps/mesoscale-explorer/data/mmcif/model.ts
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:06 -07:00
ludovic autin
cf7f9d6aba lint 2024-06-14 11:05:02 -07:00
ludovic autin
6af1bc5def snapshot description customized component to support more advanced markdown command for highlighting and selection 2024-06-14 10:57:00 -07:00
ludovic autin
936808e271 using driver.js 1.3.1 design an introductory tour of the user interface. 2024-06-14 10:30:54 -07:00
ludovic autin
8b985d0424 rename the descriptino holder; separate from the selection UIComponent and create its own component. Change its position to below the tree. 2024-06-14 09:40:05 -07:00
ludovic autin
b290cf121a Merge branch 'master' into me_selection_description 2024-06-14 08:57:30 -07:00
Alexander Rose
89df6cec42 support reading score from B-factor in pLDDT color theme 2024-06-13 23:28:14 -07:00
Alexander Rose
fc5fc7fcdb Merge pull request #1134 from molstar/occlusion-fixes
fixes for SSAO edge artifacts
2024-06-13 23:22:17 -07:00
Alexander Rose
a1de5bb304 Merge branch 'master' of https://github.com/molstar/molstar into occlusion-fixes 2024-06-13 23:17:14 -07:00
Alexander Rose
25c8a41e91 selection description tweaks 2024-06-13 22:54:34 -07:00
ludovic autin
959249b572 added getCellDescription to generate the markdown for the given cell. For mmCIF files use the entity row info to genereate a description. 2024-06-13 11:16:55 -07:00
Alexander Rose
44610b8b1a update cif schema 2024-06-12 21:50:49 -07:00
Alexander Rose
4070453209 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1157 2024-06-12 21:48:10 -07:00
Alexander Rose
cf2193f4fc Merge pull request #1156 from corredD/me_camera_behavior
mesoscale explorer centerOnly behavior
2024-06-12 21:41:46 -07:00
Alexander Rose
bfc0a3d1fe changelog 2024-06-12 21:41:28 -07:00
Alexander Rose
53a2155d8c Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1156 2024-06-12 21:40:04 -07:00
Alexander Rose
f22121521b Merge pull request #1152 from corredD/me_illustrative
enable illustrative color scheme in the tree
2024-06-12 19:52:20 -07:00
Alexander Rose
755655d067 add illustrative coloring option 2024-06-12 19:45:53 -07:00
ludovic autin
41ab186fd2 ME - entities can have description text retriev in the file in the entity field or in the manifest. The descripion appear for the current selection in the left panel and in the tips when mouse hove in the viewport. The markdown will use the same handling as the Snapshot description defined in the me_ui_label PR. 2024-06-12 12:37:35 -07:00
ludovic autin
9039c653cb centerOnly toggle is actually used. Its is exposed as a Key toggle 'c'. How can we expose all the MesoFocusLociProps in the UI ? So that user can easily toggle the centerOnly and change the transition duration... 2024-06-12 11:00:44 -07:00
ludovic autin
dde3f4ecff Merge branch 'master' into me_illustrative 2024-06-12 09:39:31 -07:00
ludovic autin
64cd05cc14 hide toggle in custom type 2024-06-11 10:24:18 -07:00
David Sehnal
593e8f4993 Improve camera interpolation (#1153)
* improve camera interpolation

* typos
2024-06-11 14:19:19 +02:00
ludovic autin
d2d9eb622f preset illustrative: false, 2024-06-10 10:11:27 -07:00
ludovic autin
ce9883517f illustrative scheme as a boolean. 2024-06-10 09:56:15 -07:00
Ludovic Autin
bc2afe1d68 Merge commit '5a2ee03b48e1822c8843d65eb18d578af0579b9f' into me_illustrative 2024-06-08 09:09:23 -07:00
Alexander Rose
5a2ee03b48 lint fix 2024-06-07 22:38:26 -07:00
ludovic autin
2d86c76788 no multispace 2024-06-07 16:56:51 -07:00
ludovic autin
7f8995a4d8 " 2024-06-07 16:50:01 -07:00
ludovic autin
64ab8bf78d enable illustrative color scheme in the tree 2024-06-07 16:44:29 -07:00
Alexander Rose
bf9663e177 handle near clip in ssao-blur 2024-06-06 23:09:02 -07:00
David Sehnal
fb729446e2 Adjust quick styles panel (#1143)
* adjust quick styles panel

* header

* Update src/mol-plugin-ui/structure/quick-styles.tsx

Co-authored-by: Paul Pillot <116235895+papillot@users.noreply.github.com>

---------

Co-authored-by: Paul Pillot <116235895+papillot@users.noreply.github.com>
2024-06-05 08:56:37 +02:00
Neli Fonseca
3e4082bf6e Decompress gzip volume (#1146)
* Created DecompressGzip transformer to render map.gz volumes correctly

* Updated changelog

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/transforms/data.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Fixes bug and updated changelog

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/apps/viewer/app.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Handle deflateData into string

* Update src/apps/viewer/app.ts

* Update src/mol-plugin-state/transforms/data.ts

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-04 12:14:49 +02:00
Alexander Rose
7148c7197b fixes for SSAO edge artifacts
- add `reuseOcclusion` parameter to multi-sample pass
- add `blurBias` parameter to occlusion pass
2024-05-16 23:48:57 -07:00
giagitom
d2192d609a Fix initial and final caps 2024-05-15 15:58:22 +02:00
Gianluca Tomasello
46ad8f495f Merge branch 'master' into tubular-alpha-helices-round-cap 2024-05-14 19:22:20 +02:00
giagitom
65b2b69a64 Smooth normals 2024-05-14 19:20:36 +02:00
Gianluca Tomasello
0d9d173ef4 Merge branch 'master' into quantized-light 2024-05-13 21:02:21 +02:00
giagitom
6eaf8e1911 Adding tweaks 2024-05-13 20:59:42 +02:00
giagitom
3220ab6118 Add round caps on tubular alpha helices 2024-05-07 17:04:18 +02:00
giagitom
5c882f1aa5 Added author in headers 2024-05-06 16:19:43 +02:00
giagitom
2320518b87 Add quantized lighting option 2024-05-06 14:58:42 +02:00
223 changed files with 7863 additions and 2897 deletions

View File

@@ -5,14 +5,112 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v4.3.0] - 2023-05-26
## [v4.7.0] - 2024-09-29
- Add illumination mode
- Path-traced SSGI
- Automatic thickness (estimate)
- Base thickness as max(backface depth) - min(frontface depth)
- Per object density factor to adjust thickness
- Progressively trace samples to keep viewport interactive
- Toggle on/off by pressing "G"
- `illumination` Viewer GET param
- Enables dXrayShaded define when rendering depth
- Fix handling of PDB files that have chains with same id separated by TER record (#1245)
- Sequence Panel: Improve visuals of unmodeled sequence positions (#1248)
- Fix no-compression xtc parser (#1258)
- Mol2 Reader: Fix mol2 status_bit read error (#1251)
- Fix shadows with multiple lights
- Fix impostor sphere interior normal when using orthographic projection
- Add `resolutionMode` parameter to `Canvas3DContext`
- `scaled`, divides by `devicePixelRatio`
- `native`, no changes
- Add `CustomProperty.Context.errorContext` to support reporting errors during loading of custom properties (#1254)
- Use in MolViewSpec extension
- Mesoscale Explorer: fix color & style issues
- Remove use of deprecated SASS explicit color functions
- Allow "Components" section to display nested components created by "Apply Action > Selection".
## [v4.6.0] - 2024-08-28
- Add round-caps option on tubular alpha helices
- Fix missing Sequence UI update on state object removal (#1219)
- Improved prmtop format support (CTITLE, %COMMENT)
- Avoid calculating bonds for water units when `ignoreHydrogens` is on
- Add `Water` trait to `Unit`
- Improve entity-id coloring for structures with multiple models from the same source (#1221)
- Wrap screenshot & image generation in a `Task`
- AlphaFold DB: Add BinaryCIF support when fetching data
- PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
- Fix polymer-gap visual coloring with cartoon theme
- Add formal-charge color theme (#328)
- Add more coloring options to cartoon theme
- Use `CompressionStream` Browser API when available
- Add `pdbx_structure_determination_methodology` mmcif field and `Model` helpers
- Fix cartoon representation not updated when secondary structure changes
- Add Zhang-Skolnick secondary-structure assignment method which handles coarse-grained models (#49)
- Calculate bonds for coarse-grained models
- VolumeServer: Add `health-check` endpoint + `healthCheckPath` config prop to report service health
- ModelServer: Add `health-check` endpoint + `healthCheckPath` config prop to report service health
## [v4.5.0] - 2024-07-28
- Separated postprocessing passes
- Take into account explicit hydrogens when computing hydrogen bonds
- Fix DoF with pixel ratios =! 1
- Fix DoF missing transparent depth
- Fix trackball pinch zoom and add pan
- Fix aromatic link rendering when `adjustCylinderLength` is true
- Change trackball animate spin speed unit to radians per second
- Fix `mol-plugin-ui/skin/base/components/misc.scss` syntax to be in line with latest Sass syntax
- Handle missing theme updates
- Fix trajectory-index color-theme not always updated (#896)
- Fix bond cylinders not updated on size-theme change with `adjustCylinderLength` enabled (#1215)
- Use `OES_texture_float_linear` for SSAO when available
## [v4.4.1] - 2024-06-30
- Clean `solidInterior` transparent cylinders
- Create a transformer to deflate compressed data
- Adjust Quick Styles panel button labels
- Improve camera interpolation code (interpolate camera rotation instead of just position)
- Mesoscale Explorer
- Add `illustrative` coloring option
- Press 'C' to toggle between center and zoom & center on click
- Add entities selection description
- Clicking a leaf node in the right panel tree will center each instance in turn
- Add measurement controls to right panel
- Mouse left click on label with snapshot key will load snapshot
- Mouse hover over label with protein name highlight entities with the same name
- Custom ViewportSnapshotDescription with custom MarkdowAnchor
- \# other snapshots with a given key \[...](#key)
- i highlight a protein with a given NAME \[...](iNAME)
- g highlight a group with a given group type and group name \[...](ggrouptype.groupname)
- h URLs with a given link \[...](http...)
- Snapshot description panel window size and text can be resized and hidden with new icons
- Add styles controls to right panel
- Add viewport settings to left panel
- Add app info component to left panel with interactive tour and doc link
- Fixes SSAO edge artifacts (#1122)
- Add `reuseOcclusion` parameter to multi-sample pass
- Add `blurDepthBias` parameter to occlusion pass
- Handle near clip in SSAO blur
- Support reading score from B-factor in pLDDT color theme
- Add Cel-shading support
- `celShaded` geometry parameter
- `celSteps` renderer parameter
- Add the ability to customize the Snapshot Description component via `PluginUISpec.components.viewport.snapshotDescription`
- Add `doNotDisposeCanvas3DContext` option to `PluginContext.dispose`
- Remove support for density data from edmaps.rcsb.org
## [v4.3.0] - 2024-05-26
- Fix State Snapshots export animation (#1140)
- Add depth of field (dof) postprocessing effect
- Add `SbNcbrTunnels` extension for for visualizing tunnels in molecular structures from ChannelsDB (more info in [tunnels.md](./docs/docs/extensions/tunnels.md))
- Fix edge case in minimizing RMSD transform computation
## [v4.2.0] - 2023-05-04
## [v4.2.0] - 2024-05-04
- Add emissive material support
- Add bloom post-processing
@@ -30,7 +128,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Fix SSAO artifacts (@corredD, #1082)
- Fix bumpiness artifacts (#1107, #1084)
## [v4.1.0] - 2023-03-31
## [v4.1.0] - 2024-03-31
- Add `VolumeTransform` to translate/rotate a volume like in a structure superposition
- Fix BinaryCIF encoder edge cases caused by re-encoding an existing BinaryCIF file
@@ -41,13 +139,13 @@ Note that since we don't clearly distinguish between a public and private interf
- This can give results similar to pymol's surface_ramp_above_mode=1
- Add `rotation` parameter to skybox background
## [v4.0.1] - 2023-02-19
## [v4.0.1] - 2024-02-19
- Fix BinaryCIF decoder edge cases. Fixes mmCIF model export from data provided by ModelServer.
- MolViewSpec extension: support for MVSX file format
- Revert "require WEBGL_depth_texture extension" & "remove renderbuffer use"
## [v4.0.0] - 2023-02-04
## [v4.0.0] - 2024-02-04
- Add Mesoscale Explorer app for investigating large systems
- [Breaking] Remove `cellpack` extension (superseded by Mesoscale Explorer app)
@@ -83,7 +181,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Add stochastic/dithered transparency to fade overlapping LODs in and out
- Add "Automatic Detail" preset that shows surface/cartoon/ball & stick based on camera distance
## [v3.45.0] - 2023-02-03
## [v3.45.0] - 2024-02-03
- Add color interpolation to impostor cylinders
- MolViewSpec components are applicable only when the model has been loaded from MolViewSpec
@@ -97,7 +195,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Support `disableInteractiveUpdates` to only trigger updates once the control loses focus
- Move dependencies related to the headless context from optional deps to optional peer deps
## [v3.44.0] - 2023-01-06
## [v3.44.0] - 2024-01-06
- Add new `cartoon` visuals to support atomic nucleotide base with sugar
- Add `thicknessFactor` to `cartoon` representation for scaling nucleotide block/ring/atomic-fill visuals

View File

@@ -107,6 +107,7 @@ entity.id
entity.type
entity.src_method
entity.pdbx_description
entity.pdbx_parent_entity_id
entity.formula_weight
entity.pdbx_number_of_molecules
entity.details
@@ -262,6 +263,7 @@ software.version
struct.entry_id
struct.title
struct.pdbx_descriptor
struct.pdbx_structure_determination_methodology
struct_asym.id
struct_asym.pdbx_blank_PDB_chainid_flag
@@ -366,18 +368,43 @@ struct_site.details
struct_site_gen.id
struct_site_gen.site_id
struct_site_gen.pdbx_num_res
struct_site_gen.label_comp_id
struct_site_gen.auth_asym_id
struct_site_gen.auth_atom_id
struct_site_gen.auth_comp_id
struct_site_gen.auth_seq_id
struct_site_gen.details
struct_site_gen.label_alt_id
struct_site_gen.label_asym_id
struct_site_gen.label_atom_id
struct_site_gen.label_comp_id
struct_site_gen.label_seq_id
struct_site_gen.pdbx_auth_ins_code
struct_site_gen.auth_comp_id
struct_site_gen.auth_asym_id
struct_site_gen.auth_seq_id
struct_site_gen.label_atom_id
struct_site_gen.label_alt_id
struct_site_gen.pdbx_num_res
struct_site_gen.symmetry
struct_site_gen.details
struct_site_keywords.site_id
struct_site_keywords.text
struct_mon_prot_cis.pdbx_id
struct_mon_prot_cis.auth_asym_id
struct_mon_prot_cis.auth_comp_id
struct_mon_prot_cis.auth_seq_id
struct_mon_prot_cis.label_alt_id
struct_mon_prot_cis.label_asym_id
struct_mon_prot_cis.label_comp_id
struct_mon_prot_cis.label_seq_id
struct_mon_prot_cis.pdbx_PDB_ins_code
struct_mon_prot_cis.pdbx_PDB_ins_code_2
struct_mon_prot_cis.pdbx_PDB_model_num
struct_mon_prot_cis.pdbx_auth_asym_id_2
struct_mon_prot_cis.pdbx_auth_comp_id_2
struct_mon_prot_cis.pdbx_auth_ins_code
struct_mon_prot_cis.pdbx_auth_ins_code_2
struct_mon_prot_cis.pdbx_auth_seq_id_2
struct_mon_prot_cis.pdbx_label_asym_id_2
struct_mon_prot_cis.pdbx_label_comp_id_2
struct_mon_prot_cis.pdbx_label_seq_id_2
struct_mon_prot_cis.pdbx_omega_angle
symmetry.entry_id
symmetry.cell_setting
1 atom_sites.entry_id
107 entity_poly.nstd_linkage entity_poly.type
108 entity_poly.nstd_monomer entity_poly.nstd_linkage
109 entity_poly.pdbx_seq_one_letter_code entity_poly.nstd_monomer
110 entity_poly.pdbx_seq_one_letter_code
111 entity_poly.pdbx_seq_one_letter_code_can
112 entity_poly.pdbx_strand_id
113 entity_poly.pdbx_target_identifier
263 struct_conn.ptnr1_label_seq_id struct_conn.ptnr1_label_comp_id
264 struct_conn.ptnr1_label_atom_id struct_conn.ptnr1_label_seq_id
265 struct_conn.pdbx_ptnr1_label_alt_id struct_conn.ptnr1_label_atom_id
266 struct_conn.pdbx_ptnr1_label_alt_id
267 struct_conn.pdbx_ptnr1_PDB_ins_code
268 struct_conn.pdbx_ptnr1_standard_comp_id
269 struct_conn.ptnr1_symmetry
368 pdbx_reference_entity_link.details symmetry.space_group_name_H-M
369 pdbx_reference_entity_list.prd_id pdbx_molecule.instance_id
370 pdbx_reference_entity_list.ref_entity_id pdbx_molecule.prd_id
371 pdbx_reference_entity_list.component_id pdbx_molecule.asym_id
372 pdbx_reference_entity_list.type pdbx_molecule_features.prd_id
373 pdbx_molecule_features.name
374 pdbx_molecule_features.type
375 pdbx_molecule_features.class
376 pdbx_molecule_features.details
377 pdbx_reference_entity_list.details pdbx_reference_entity_link.prd_id
378 pdbx_reference_entity_link.link_id
379 pdbx_reference_entity_link.link_class
380 pdbx_reference_entity_poly_link.prd_id pdbx_reference_entity_link.ref_entity_id_1
381 pdbx_reference_entity_poly_link.ref_entity_id pdbx_reference_entity_link.entity_seq_num_1
382 pdbx_reference_entity_poly_link.link_id pdbx_reference_entity_link.comp_id_1
pdbx_reference_entity_poly_link.atom_id_1
pdbx_reference_entity_poly_link.comp_id_1
pdbx_reference_entity_poly_link.entity_seq_num_1
pdbx_reference_entity_poly_link.atom_id_2
383 pdbx_reference_entity_poly_link.comp_id_2 pdbx_reference_entity_link.atom_id_1
384 pdbx_reference_entity_poly_link.entity_seq_num_2 pdbx_reference_entity_link.ref_entity_id_2
385 pdbx_reference_entity_link.entity_seq_num_2
386 pdbx_reference_entity_link.comp_id_2
387 pdbx_reference_entity_link.atom_id_2
388 pdbx_reference_entity_link.value_order
389 pdbx_reference_entity_link.component_1
390 pdbx_reference_entity_link.component_2
391 pdbx_reference_entity_link.details
392 pdbx_reference_entity_list.prd_id
393 pdbx_reference_entity_list.ref_entity_id
394 pdbx_reference_entity_list.component_id
395 pdbx_reference_entity_list.type
396 pdbx_reference_entity_list.details
397 pdbx_reference_entity_poly_link.prd_id
398 pdbx_reference_entity_poly_link.ref_entity_id
399 pdbx_reference_entity_poly_link.link_id
400 pdbx_reference_entity_poly_link.atom_id_1
401 pdbx_reference_entity_poly_link.comp_id_1
402 pdbx_reference_entity_poly_link.entity_seq_num_1
403 pdbx_reference_entity_poly_link.atom_id_2
404 pdbx_reference_entity_poly_link.comp_id_2
405 pdbx_reference_entity_poly_link.entity_seq_num_2
406 pdbx_reference_entity_poly_link.value_order
407 pdbx_reference_entity_poly_link.component_id
408 pdbx_reference_entity_poly_link.value_order pdbx_struct_assembly.id
409 pdbx_reference_entity_poly_link.component_id pdbx_struct_assembly.details
410 pdbx_struct_assembly.id pdbx_struct_assembly.method_details

View File

@@ -22,7 +22,7 @@
* Discontinuous chains, i.e. gaps in the sequence (3sn6)
* Lots of sheets (1cbs)
* DNA (2np2, 1d66)
* C-alpha only (2rcj)
* C-alpha only (2RCJ, 6ZIG, 5AJ2)
* Not cyclic, but termini are backbone-only and within distance but seqIds are not compatible (6SW3)
* Close backbone atoms but not linked (e.g. 4HIV)
* Non-standard residues

View File

@@ -0,0 +1,193 @@
# Building a Custom Library
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page.
## Setup
- Create a new npm/yarn package
- Install `molstar` and `esbuild` packages
```
mkdir molstar-lib
cd molstar-lib
npm init
npm install molstar
npm install esbuild --save-dev
```
## Example Library Code
Create new file `src/index.ts` (or `.js` if you don't want to use TypeScript):
```ts
import { DefaultPluginSpec, PluginSpec } from 'molstar/lib/mol-plugin/spec';
import { PluginContext } from 'molstar/lib/mol-plugin/context';
export async function initViewer(element: string | HTMLDivElement, options?: { spec?: PluginSpec }) {
const parent = typeof element === 'string' ? document.getElementById(element)! as HTMLDivElement : element;
const canvas = document.createElement('canvas') as HTMLCanvasElement;
parent.appendChild(canvas);
const spec = options?.spec ?? DefaultPluginSpec();
const plugin = new PluginContext(spec);
await plugin.init();
plugin.initViewer(canvas, parent);
return plugin;
}
export async function loadStructure(
plugin: PluginContext,
url: string,
options?: { format?: string, isBinary?: boolean }
) {
const data = await plugin.builders.data.download(
{ url, isBinary: options?.isBinary }
);
const trajectory = await plugin.builders.structure.parseTrajectory(
data,
options?.format ?? 'mmcif' as any
);
const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
return preset;
}
```
## Building the Library
Add new commands to the `scripts` section of the `package.json` file
```json
"scripts": {
"build": "esbuild src/index.ts --bundle --outfile=./build/js/index.js --global-name=molstarLib",
"watch": "esbuild src/index.ts --bundle --outfile=./build/js/index.js --global-name=molstarLib --watch"
}
```
and run the command `npm run build` (or `watch` for interactive development experience). This will create `build/js/index.js` file which can be imported with a `<script>` tag and the exported functions called view the `molstarLib` prefix (you can customize this parameter).
## Using the Library
Create file `build/index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Mol* Library Example</title>
</head>
<style>
#viewer {
position: absolute;
width: 800px;
height: 600px;
}
</style>
<script type="text/javascript" src="./js/index.js"></script>
<body>
<div id="viewer"></div>
<script type="text/javascript">
async function init() {1
const plugin = await molstarLib.initViewer("viewer");
await molstarLib.loadStructure(
plugin,
"https://models.rcsb.org/4hhb.bcif",
{ isBinary: true }
);
}
init();
</script>
</body>
</html>
```
After opening `index.html` in a browser, you should see
![lib-example](lib-example.png)
## Using Mol* React UI
The above example does not make use of the default Mol\* React UI and any UI components are therefore the author's responsibility. The below examples show how to (re)use the Mol\* React UI.
- Create `src/ui.tsx`:
```tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { DefaultPluginUISpec, PluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
import { Plugin } from 'molstar/lib/mol-plugin-ui/plugin';
export async function initViewerUI(element: string | HTMLDivElement, options?: { spec?: PluginUISpec }) {
const parent = typeof element === 'string' ? document.getElementById(element)! as HTMLDivElement : element;
const spec = { ...DefaultPluginUISpec(), ...options?.spec };
const plugin = new PluginUIContext(spec);
await plugin.init();
createRoot(parent).render(<Plugin plugin={plugin} />)
return plugin;
}
export async function loadStructure(plugin: PluginUIContext, url: string, options?: { format?: string, isBinary?: boolean }) {
const data = await plugin.builders.data.download({ url, isBinary: options?.isBinary });
const trajectory = await plugin.builders.structure.parseTrajectory(data, options?.format ?? 'mmcif' as any);
await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
}
```
- Create `src/style.scss`:
```scss
@import '../node_modules/molstar/lib/mol-plugin-ui/skin/light.scss';
```
- Create `build/ui.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Mol* UI Library Example</title>
</head>
<link rel="stylesheet" type="text/css" href="css/style.css" />
<style>
#viewer {
position: absolute;
inset: 0;
}
</style>
<script type="text/javascript" src="./js/ui.js"></script>
<body>
<div id="viewer"></div>
<script type="text/javascript">
async function init() {
const plugin = await molstarLib.initViewerUI("viewer", {
spec: {
layout: {
initial: {
isExpanded: true,
showControls: true,
},
},
}
});
await molstarLib.loadStructure(plugin, "https://models.rcsb.org/4hhb.bcif", { isBinary: true });
}
init();
</script>
</body>
</html>
```
- Install `sass`: `npm install sass -save-dev` (or use [`esbuild` plugin](https://www.npmjs.com/package/esbuild-sass-plugin) and `import` the scss file in `ui.tsx`)
- Add scripts to `package.json`:
```json
"build-ui": "esbuild src/ui.tsx --bundle --outfile=./build/js/ui.js --global-name=molstarLib",
"css": "sass src/style.scss ./build/css/style.css"
```
- Run `npm run build-ui` and `npm run css` (skip if using `esbuild-sass-plugin`)
- Opening `build/ui.html`:
![ui-example](ui-example.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

View File

@@ -32,6 +32,7 @@ nav:
- Plugin:
- Creating Instance: 'plugin/instance.md'
- Examples: plugin/examples.md
- Custom Library: 'plugin/custom-library.md'
- Selections: 'plugin/selections.md'
- Viewer State: 'plugin/viewer-state.md'
- Data State: 'plugin/data-state.md'

1822
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "4.3.0",
"version": "4.7.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -107,43 +107,47 @@
"Christian Dominguez <christian.99dominguez@gmail.com>",
"Cai Huiyu <szmun.caihy@gmail.com>",
"Ryan DiRisio <rjdiris@gmail.com>",
"Dušan Veľký <dvelky@mail.muni.cz>"
"Dušan Veľký <dvelky@mail.muni.cz>",
"Neli Fonseca <neli@ebi.ac.uk>",
"Paul Pillot <paul.pillot@tandemai.com>",
"Herman Bergwerf <post@hbergwerf.nl>",
"Eric E <etongfu@outlook.com>"
],
"license": "MIT",
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/gl": "^6.0.5",
"@types/jest": "^29.5.13",
"@types/pngjs": "^6.0.5",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.1",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"benchmark": "^2.1.4",
"concurrently": "^8.2.2",
"concurrently": "^9.0.1",
"cpx2": "^7.0.1",
"crypto-browserify": "^3.12.0",
"css-loader": "^7.1.1",
"eslint": "^8.57.0",
"css-loader": "^7.1.2",
"eslint": "^8.57.1",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^11.2.0",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jpeg-js": "^0.4.4",
"mini-css-extract-plugin": "^2.9.0",
"mini-css-extract-plugin": "^2.9.1",
"path-browserify": "^1.0.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.76.0",
"sass-loader": "^14.2.1",
"simple-git": "^3.24.0",
"sass": "^1.79.4",
"sass-loader": "^16.0.2",
"simple-git": "^3.27.0",
"stream-browserify": "^3.0.0",
"style-loader": "^4.0.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.5",
"webpack": "^5.91.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
@@ -151,23 +155,23 @@
"@types/benchmark": "^2.1.5",
"@types/compression": "1.7.5",
"@types/express": "^4.17.21",
"@types/node": "^18.19.31",
"@types/node": "^18.19.54",
"@types/node-fetch": "^2.6.11",
"@types/swagger-ui-dist": "3.30.4",
"@types/swagger-ui-dist": "3.30.5",
"argparse": "^2.0.1",
"body-parser": "^1.20.2",
"body-parser": "^1.20.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.19.2",
"express": "^4.21.0",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.1.1",
"immutable": "^4.3.5",
"immutable": "^4.3.7",
"io-ts": "^2.2.21",
"node-fetch": "^2.7.0",
"react-markdown": "^9.0.1",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.17.2",
"tslib": "^2.6.2",
"swagger-ui-dist": "^5.17.14",
"tslib": "^2.7.0",
"util.promisify": "^1.1.2",
"xhr2": "^0.2.1"
},

View File

@@ -42,7 +42,7 @@ function copyViewer() {
function copyMe() {
console.log('\n###', 'copy me files');
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
const meDeployPath = path.resolve(localPath, 'me/');
const meDeployPath = path.resolve(localPath, 'me/viewer/');
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
addAnalytics(path.resolve(meDeployPath, 'index.html'));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -46,6 +46,7 @@ function occlusionStyle(plugin: PluginContext) {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'on', params: {
blurKernelSize: 15,
blurDepthBias: 0.5,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -30,6 +30,7 @@ import { Asset } from '../../mol-util/assets';
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
import { AnimateStateSnapshots } from '../../mol-plugin-state/animation/built-in/state-snapshots';
import { MesoViewportSnapshotDescription } from './ui/entities';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
@@ -46,7 +47,9 @@ export type ExampleEntry = {
export type MesoscaleExplorerState = {
examples?: ExampleEntry[],
graphicsMode: GraphicsMode,
illumination: boolean,
stateRef?: string,
driver?: any,
stateCache: { [k: string]: any },
}
@@ -90,7 +93,9 @@ const DefaultMesoscaleExplorerOptions = {
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
graphicsMode: 'quality' as GraphicsMode
graphicsMode: 'quality' as GraphicsMode,
illumination: false,
driver: undefined
};
type MesoscaleExplorerOptions = typeof DefaultMesoscaleExplorerOptions;
@@ -170,6 +175,9 @@ export class MesoscaleExplorer {
right: RightPanel,
},
remoteState: 'none',
viewport: {
snapshotDescription: MesoViewportSnapshotDescription,
}
},
config: [
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
@@ -207,7 +215,12 @@ export class MesoscaleExplorer {
onBeforeUIRender: async plugin => {
let examples: MesoscaleExplorerState['examples'] = undefined;
try {
examples = await plugin.fetch({ url: './examples/list.json', type: 'json' }).run();
examples = await plugin.fetch({ url: '../examples/list.json', type: 'json' }).run();
// extend the array with file tour.json if it exists
const tour = await plugin.fetch({ url: '../examples/tour.json', type: 'json' }).run();
if (tour) {
examples = examples?.concat(tour);
}
} catch (e) {
console.log(e);
}
@@ -215,6 +228,8 @@ export class MesoscaleExplorer {
(plugin.customState as MesoscaleExplorerState) = {
examples,
graphicsMode: o.graphicsMode,
illumination: o.illumination,
driver: o.driver,
stateCache: {},
};

View File

@@ -11,10 +11,12 @@ import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observ
import { Binding } from '../../../mol-util/binding';
import { PluginCommands } from '../../../mol-plugin/commands';
import { Sphere3D } from '../../../mol-math/geometry';
import { StructureElement } from '../../../mol-model/structure';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
const DefaultMesoFocusLociBindings = {
clickCenter: Binding([
@@ -23,13 +25,14 @@ const DefaultMesoFocusLociBindings = {
clickCenterFocus: Binding([
Trigger(B.Flag.Secondary, M.create()),
], 'Camera center and focus', 'Click element using ${triggers}'),
keyCenterOnly: Binding([Key('C')], 'Center Only Toggle', 'Press ${triggers}'),
};
const MesoFocusLociParams = {
export const MesoFocusLociParams = {
minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
extraRadius: PD.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci' }),
durationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration' }),
centerOnly: PD.Boolean(true, { description: 'Keep current camera distance' }),
bindings: PD.Value(DefaultMesoFocusLociBindings, { isHidden: true }),
};
type MesoFocusLociProps = PD.Values<typeof MesoFocusLociParams>
@@ -47,25 +50,45 @@ export const MesoFocusLoci = PluginBehavior.create<MesoFocusLociProps>({
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const { clickCenter, clickCenterFocus } = this.params.bindings;
const { durationMs, extraRadius, minRadius } = this.params;
const { durationMs, extraRadius, minRadius, centerOnly } = this.params;
const radius = Math.max(sphere.radius + extraRadius, minRadius);
if (Binding.match(clickCenter, button, modifiers)) {
// left mouse button
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
if (StructureElement.Loci.is(current.loci)) {
if (centerOnly) {
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
} else {
this.ctx.managers.camera.focusSphere(sphere, this.params);
}
}
} else if (Binding.match(clickCenterFocus, button, modifiers)) {
// right mouse button
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
if (centerOnly) {
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
canvas3d.requestCameraReset({ durationMs, snapshot });
} else {
this.ctx.managers.camera.focusSphere(sphere, this.params);
}
}
});
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
canvas3d.requestCameraReset({ durationMs, snapshot });
this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ code, key, modifiers }) => {
if (!this.ctx.canvas3d) return;
const b = { ...DefaultMesoFocusLociBindings, ...this.params.bindings };
const { centerOnly } = this.params;
if (Binding.matchKey(b.keyCenterOnly, code, modifiers, key)) {
this.params.centerOnly = !centerOnly;
}
});
}

View File

@@ -16,12 +16,16 @@ import { StateTreeSpine } from '../../../mol-state/tree/spine';
import { Representation } from '../../../mol-repr/representation';
import { MarkerAction } from '../../../mol-util/marker-action';
import { PluginContext } from '../../../mol-plugin/context';
import { MesoscaleState, expandAllGroups, getCellDescription, getEveryEntity } from '../data/state';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const DefaultMesoSelectLociBindings = {
click: Binding([
Trigger(B.Flag.Primary, M.create())
], 'Click', 'Click element using ${triggers}'),
clickToggleSelect: Binding([
Trigger(B.Flag.Primary, M.create({ shift: true })),
Trigger(B.Flag.Primary, M.create({ control: true })),
@@ -63,15 +67,36 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
const { clickToggleSelect } = this.params.bindings;
const { click, clickToggleSelect } = this.params.bindings;
if (Binding.match(clickToggleSelect, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
this.ctx.managers.interactivity.lociSelects.deselectAll();
return;
}
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
const d = getCellDescription(cell!);
MesoscaleState.set(this.ctx, { focusInfo: `${d}` });
}
}
if (Binding.match(click, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
MesoscaleState.set(this.ctx, { focusInfo: '', filter: '' });
return;
}
const snapshotKey = current.repr?.props?.snapshotKey?.trim() ?? '';
if (snapshotKey) {
this.ctx.managers.snapshot.applyKey(snapshotKey);
} else {
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
const d = getCellDescription(cell!);
MesoscaleState.set(this.ctx, { focusInfo: `${d}`, filter: `${cell?.obj?.label}` });
expandAllGroups(this.ctx);
}
}
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
@@ -87,22 +112,41 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
this.ctx.managers.interactivity.lociHighlights.clearHighlights();
return;
}
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
if (StructureElement.Loci.is(current.loci)) {
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
}
}
}
if (Loci.isEmpty(current.loci)) {
this.ctx.behaviors.labels.highlight.next({ labels: [] });
this.ctx.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
} else {
const labels: string[] = [];
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
labels.push(cell?.obj?.label || 'Unknown');
const d = getCellDescription(cell!);
labels.push(d);
} else {
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
if (loci.kind === 'group-loci') {
if ('shape' in current.loci && current.loci.shape.geometry.kind === 'text') {
const qname = current.repr?.props.customText;
// highlight protein with same name
const entities = getEveryEntity(this.ctx, qname);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
this.ctx.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
labels.push(loci.shape.getLabel(0, 0));
}
}
this.ctx.behaviors.labels.highlight.next({ labels });
}

View File

@@ -161,15 +161,12 @@ const CellpackStructure = PluginStateTransform.BuiltIn({
const unitsByEntity = getUnitsByEntity(parent);
const units = unitsByEntity.get(idx) || [];
// if (!unitsByEntity.get(idx)) {
// console.log(entities.data.pdbx_description.value(idx));
// }
const structure = Structure.create(units);
const description = entities.data.pdbx_description.value(idx)[0] || 'model';
const label = description.split('.').at(-1) || a.label;
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
const description_label = entities.data.pdbx_description.value(idx)[0] || 'model';
const label = description_label.split('.').at(-1) || a.label;
const description = entities.data.pdbx_parent_entity_id.value(idx) || label;
return new PSO.Molecule.Structure(structure, { label, description: description }); // `${a.description}`
});
},
dispose({ b }) {

View File

@@ -96,12 +96,12 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
const compRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'comp:', label: 'compartment', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const funcRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'func:', label: 'function', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (entities._rowCount > 1) {
@@ -159,7 +159,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
parent.cell!.state.isCollapsed = false;
const group = await state.build()
.to(parent)
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:comp:${n}`, `comp:${p}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
compGroups.set(n, group);
}
@@ -171,7 +171,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
const color = colorIdx !== undefined ? baseFuncColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(funcRoot)
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:func:${f}`, 'func:'], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
funcGroups.set(f, group);
}

View File

@@ -76,6 +76,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
params: {
instances: PD.Value<GenericInstances<Asset>>(EmptyInstances),
label: PD.Optional(PD.Text('')),
description: PD.Optional(PD.Text('')),
cellSize: PD.Numeric(500, { min: 0, max: 10000, step: 100 }),
}
})({
@@ -111,7 +112,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
structure = assembler.getStructure();
}
const props = { label, description: Structure.elementDescription(structure) };
const props = { label, description: params.description || Structure.elementDescription(structure) };
return new SO.Molecule.Structure(structure, props);
});
},

View File

@@ -11,7 +11,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
import { Color } from '../../../../mol-util/color';
import { utf8Read } from '../../../../mol-io/common/utf8';
import { Mat3, Quat, Vec3 } from '../../../../mol-math/linear-algebra';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { ColorNames } from '../../../../mol-util/color/names';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { ParseCif, ParsePly, ReadFile } from '../../../../mol-plugin-state/transforms/data';
@@ -124,7 +124,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
async function addGroup(g: GenericGroup, cell: StateObjectSelector, parent: string) {
const group = await state.build()
.to(cell)
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, description: g.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
if (g.children) {
@@ -137,7 +137,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
for (const r of manifest.roots) {
const root = await state.build()
.toRoot()
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, description: r.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (r.children) {
@@ -193,11 +193,12 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
const file = Asset.File(new File([t], ent.file));
const color = ColorNames.skyblue;
const label = ent.label || ent.file.split('.')[0];
const sizeFactor = ent.sizeFactor || 1;
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
const instances = ent.instances && getAssetInstances(ent.instances);
const description = ent.description;
const label = ent.label || ent.file.split('.')[0];
build = build
.toRoot()
.apply(ReadFile, { file, label, isBinary });
@@ -233,7 +234,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
build = build
.apply(ModelFromTrajectory, { modelIndex: 0 })
.apply(StructureFromGeneric, { instances, label })
.apply(StructureFromGeneric, { instances, label, description })
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode, clipVariant), { tags });
} else if (['ply'].includes(info.ext)) {
if (['ply'].includes(info.ext)) {
@@ -249,10 +250,6 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
}
}
await build.commit();
const rootId = `${manifest.roots[0].id}:`;
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
await updateColors(plugin, values, rootId, '');
} catch (e) {
console.error(e);
plugin.log.error(e);

View File

@@ -186,9 +186,10 @@ const MmcifStructure = PluginStateTransform.BuiltIn({
} else {
structure = Structure.create(units);
}
// could also use _struct_ref.pdbx_db_accession to point to uniprot with _struct_ref.db_name == UNP
const label = entities.data.pdbx_description.value(idx).join(', ') || 'model';
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
const description = `*Entity id* ${entities.data.id.value(idx)} *src_method* ${entities.data.src_method.value(idx)} *type* ${entities.data.type.value(idx)}`;
return new PSO.Molecule.Structure(structure, { label, description: description });
});
},
update({ newParams, oldParams }) {

View File

@@ -114,7 +114,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
const entRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'ent:', label: 'entity', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const getEntityType = (i: number) => {
@@ -148,7 +148,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
const color = colorIdx !== undefined ? baseEntColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(entRoot)
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
entGroups.set(t, group);
}

View File

@@ -39,8 +39,10 @@ const StructureFromPetworld = PluginStateTransform.BuiltIn({
const { frame } = s.model.sourceData.data;
const pdbx_model = frame.categories.pdbx_model.getField('name')!;
const pdbx_description = frame.categories.pdbx_model.getField('description')!;
const description = pdbx_description ? pdbx_description.str(params.modelIndex) : Structure.elementDescription(s);
const label = pdbx_model.str(params.modelIndex);
const props = { label, description: Structure.elementDescription(s) };
const props = { label, description: description };
return new SO.Molecule.Structure(s, props);
});
},

View File

@@ -97,12 +97,12 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
const group = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: ['group:ent:'], state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
await state.build()
.to(group)
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', illustrative: false, value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: ['group:ent:mem', 'ent:', '__no_group_color__'], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
const colors = getDistinctBaseColors(other.length, 0);
@@ -115,13 +115,13 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
build = build
.to(cell)
.apply(StructureFromPetworld, membrane[i])
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgrey, graphicsMode), { tags: [`ent:mem`] });
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgrey, graphicsMode), { tags: ['ent:mem', '__no_group_color__'] });
}
for (let i = 0, il = other.length; i < il; ++i) {
build = build
.to(cell)
.apply(StructureFromPetworld, other[i])
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: [`ent:`] });
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: ['ent:'] });
}
await build.commit();
} catch (e) {

View File

@@ -24,6 +24,7 @@ import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/rep
import { assertUnreachable } from '../../../mol-util/type-helpers';
import { MesoscaleExplorerState } from '../app';
import { saturate } from '../../../mol-math/interpolate';
import { Material } from '../../../mol-util/material';
function getHueRange(hue: number, variability: number) {
let min = hue - variability;
@@ -106,6 +107,7 @@ export function getDistinctBaseColors(count: number, shift: number, props?: Part
export const ColorParams = {
type: PD.Select('generate', PD.arrayToOptions(['generate', 'uniform', 'custom'])),
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type === 'custom' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
@@ -119,6 +121,7 @@ export const ColorValueParam = PD.Color(Color(0xFFFFFF));
export const RootParams = {
type: PD.Select('custom', PD.arrayToOptions(['group-generate', 'group-uniform', 'generate', 'uniform', 'custom'])),
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type !== 'uniform' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'group-generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
@@ -132,6 +135,10 @@ export const LightnessParams = {
};
export const DimLightness = 6;
export const IllustrativeParams = {
illustrative: PD.Boolean(false, { description: 'Illustrative style' }),
};
export const OpacityParams = {
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
@@ -140,11 +147,24 @@ export const EmissiveParams = {
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
};
export const celShaded = {
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' })
};
export type celShadedProps = PD.Values<typeof celShaded>;
export const PatternParams = {
frequency: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
amplitude: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
export const StyleParams = {
ignoreLight: PD.Boolean(false, { description: 'Ignore light for stylized rendering of representations' }),
materialStyle: Material.getParam(),
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' }),
};
export const LodParams = {
lodLevels: Spheres.Params.lodLevels,
cellSize: Spheres.Params.cellSize,
@@ -250,6 +270,7 @@ export const MesoscaleGroupParams = {
index: PD.Value<number>(-1, { isHidden: true }),
tag: PD.Value<string>('', { isHidden: true }),
label: PD.Value<string>('', { isHidden: true }),
description: PD.Value<string>('', { isHidden: true }),
hidden: PD.Boolean(false),
color: PD.Group(RootParams),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
@@ -271,7 +292,7 @@ export const MesoscaleGroup = PluginStateTransform.BuiltIn({
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Apply Mesoscale Group', async () => {
return new MesoscaleGroupObject({}, { label: params.label });
return new MesoscaleGroupObject({}, { label: params.label, description: params.description });
});
},
});
@@ -318,10 +339,10 @@ export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): Lod
];
case 'ultra':
return [
{ minDistance: 1, maxDistance: 2000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 2000, maxDistance: 8000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 8000, maxDistance: 20000, overlap: 0, stride: 50, scaleBias: 2.5 },
{ minDistance: 20000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
];
default:
assertUnreachable(graphicsMode);
@@ -360,7 +381,10 @@ export const MesoscaleStateParams = {
filter: PD.Value<string>('', { isHidden: true }),
graphics: PD.Select('quality', PD.arrayToOptions(['ultra', 'quality', 'balanced', 'performance', 'custom'] as GraphicsMode[])),
description: PD.Value<string>('', { isHidden: true }),
focusInfo: PD.Value<string>('', { isHidden: true }),
link: PD.Value<string>('', { isHidden: true }),
textSizeDescription: PD.Numeric(14, { min: 1, max: 100, step: 1 }, { isHidden: true }),
index: PD.Value<number>(-1, { isHidden: true })
};
export class MesoscaleStateObject extends PSO.Create<MesoscaleState>({ name: 'Mesoscale State', typeClass: 'Object' }) { }
@@ -482,6 +506,7 @@ function getFilterMatcher(filter: string) {
}
export function getFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
if (!filter) return getEntities(plugin, tag);
const matcher = getFilterMatcher(filter);
return getEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
@@ -499,22 +524,72 @@ export function getAllEntities(plugin: PluginContext, tag?: string) {
}
export function getAllFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
if (!filter) return getAllEntities(plugin, tag);
const matcher = getFilterMatcher(filter);
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
export function getEveryEntity(plugin: PluginContext, filter?: string, tag?: string) {
if (filter) {
const matcher = getFilterMatcher(filter);
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
} else {
return getAllEntities(plugin, tag);
}
}
export function getEntityLabel(plugin: PluginContext, cell: StateObjectCell) {
return StateObjectRef.resolve(plugin.state.data, cell.transform.parent)?.obj?.label || 'Entity';
}
//
export function getCellDescription(cell: StateObjectCell) {
// markdown style for description
return '**' + cell?.obj?.label + '**\n\n' + cell?.obj?.description;
}
export function getEntityDescription(plugin: PluginContext, cell: StateObjectCell) {
const s = StateObjectRef.resolve(plugin.state.data, cell.transform.parent);
const d = getCellDescription(s!);
return d;
}
export async function updateStyle(plugin: PluginContext, options: { ignoreLight: boolean, material: Material, celShaded: boolean, illustrative: boolean }) {
const update = plugin.state.data.build();
const { ignoreLight, material, celShaded, illustrative } = options;
const entities = getAllEntities(plugin);
for (let j = 0; j < entities.length; ++j) {
update.to(entities[j]).update(old => {
if (old.type) {
const value = old.colorTheme.name === 'illustrative'
? old.colorTheme.params.style.params.value
: old.colorTheme.params.value;
const lightness = old.colorTheme.name === 'illustrative'
? old.colorTheme.params.style.params.lightness
: old.colorTheme.params.lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value, lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value, lightness } };
}
old.type.params.ignoreLight = ignoreLight;
old.type.params.material = material;
old.type.params.celShaded = celShaded;
}
});
}
await update.commit();
};
export async function updateColors(plugin: PluginContext, values: PD.Values, tag: string, filter: string) {
const update = plugin.state.data.build();
const { type, value, shift, lightness, alpha, emissive } = values;
const { type, illustrative, value, shift, lightness, alpha, emissive } = values;
if (type === 'group-generate' || type === 'group-uniform') {
const groups = getAllLeafGroups(plugin, tag);
const leafGroups = getAllLeafGroups(plugin, tag);
const rootLeafGroups = getRoots(plugin).filter(g => g.params?.values.tag === tag && getEntities(plugin, g.params?.values.tag).length > 0);
const groups = [...leafGroups, ...rootLeafGroups];
const baseColors = getDistinctBaseColors(groups.length, shift);
for (let i = 0; i < groups.length; ++i) {
@@ -531,8 +606,11 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
const c = type === 'group-generate' ? groupColors[j] : baseColors[i];
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
@@ -548,6 +626,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
update.to(g.transform.ref).update(old => {
old.color.type = type === 'group-generate' ? 'generate' : 'uniform';
old.color.illustrative = illustrative;
old.color.value = baseColors[i];
old.color.lightness = lightness;
old.color.alpha = alpha;
@@ -566,8 +645,11 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
const c = type === 'generate' ? groupColors[j] : value;
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
@@ -585,6 +667,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
for (const o of others) {
update.to(o).update(old => {
old.color.type = type === 'generate' ? 'custom' : 'uniform';
old.color.illustrative = illustrative;
old.color.value = value;
old.color.lightness = lightness;
old.color.alpha = alpha;
@@ -603,3 +686,4 @@ export function expandAllGroups(plugin: PluginContext) {
}
}
};

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
overflow: hidden;
}
#controls {
display: flex;
justify-content: center;
width: 800px;
margin-bottom: 10px;
z-index: 1;
}
button {
margin: 5px;
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
}
#viewer-container {
width: 100%;
height: 600px;
border: 1px solid #ccc;
position: relative;
}
</style>
<link rel="stylesheet" type="text/css" href="./molstar.css" />
</head>
<body>
<div id="controls">
<button onclick="loadExample('cellpack-hiv1')">Load HIV-1 Example</button>
<button onclick="loadExample('machineryoflife-tour')">Load Machinery of Life Tour</button>
<button onclick="loadExample('petworld-synvesicle')">Load Synaptic Vesicle Example</button>
</div>
<div id="viewer-container">
<div id="meso-viewer" style="position: relative; width: 100%; height: 400px;"></div>
</div>
<script src="./molstar.js"></script>
<script type="text/javascript">
let mesoExplorer;
function loadExample(example) {
if (mesoExplorer) {
mesoExplorer.loadExample(example);
}
}
molstar.MesoscaleExplorer.create('meso-viewer', {
layoutShowControls: false,
viewportShowExpand: false,
layoutIsExpanded: false,
powerPreference: 'high-performance',
graphicsMode: 'quality'
}).then(me => {
mesoExplorer = me;
me.loadExample('cellpack-hiv1'); // Load the default example on page load
window.addEventListener('unload', () => {
me.dispose();
});
});
</script>
</body>
</html>

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../extras/driver.css"/>
<title>Mol* Mesoscale Explorer</title>
<style>
* {
@@ -38,8 +39,11 @@
</head>
<body>
<div id="app"></div>
<script src="../extras/driver.js.iife.js"></script>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
const driver = window.driver ? window.driver.js.driver() : undefined;
function getParam(name, regex) {
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
@@ -56,6 +60,7 @@
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
var graphicsMode = getParam('graphics-mode', '[^&]+').trim().toLowerCase();
var illumination = getParam('illumination', '[^&]+').trim() === '1';
molstar.MesoscaleExplorer.create('app', {
layoutShowControls: !hideControls,
@@ -64,6 +69,8 @@
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
graphicsMode: graphicsMode || 'quality',
illumination: illumination,
driver: driver
}).then(me => {
var example = getParam('example', '[^&]+').trim();
if (example) {
@@ -89,7 +96,6 @@
me.loadPdbDev(pdbdev);
return;
}
window.addEventListener('unload', () => {
// to aid GC
me.dispose();
@@ -98,4 +104,4 @@
</script>
<!-- __MOLSTAR_ANALYTICS__ -->
</body>
</html>
</html>

View File

@@ -1,3 +1,5 @@
@use "sass:color";
$default-background: #2D3E50;
$font-color: #EDF1F2;
$hover-font-color: #3B9AD9;
@@ -16,14 +18,15 @@ $log-error: #FD354B;
$logo-background: rgba(0,0,0,0.75);
@function color-lower-contrast($color, $amount) {
@return darken($color, $amount);
@return color.adjust($color, $lightness: -$amount, $space: hsl);
}
@function color-increase-contrast($color, $amount) {
@return lighten($color, $amount);
@return color.adjust($color, $lightness: $amount, $space: hsl);
}
@import 'mol-plugin-ui/skin/base/base';
@import 'mol-plugin-ui/skin/base/variables';
a {
color: $font-color;
@@ -31,3 +34,35 @@ a {
color: $hover-font-color;
}
}
.msp-snapshot-description-me {
background: color.change($default-background, $alpha: 0.5, $space: rgb);
position: absolute;
height: 50vh; // 50% of the viewport height
left: 0;
top: $control-spacing + $row-height;
padding: (0.66 * $control-spacing) $control-spacing;
resize: both; /* Allows resizing in both directions */
overflow: auto; /* Adjust as needed */
a {
text-decoration: underline;
cursor: pointer;
color: $font-color;
}
ul, ol {
padding-left: $control-spacing + 4px;
}
&.hidden {
display: none;
}
&.shown {
display: block; // or 'flex', 'grid', etc. depending on your layout
}
}

View File

@@ -4,9 +4,9 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { PluginReactContext, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ControlGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg } from '../../../mol-plugin-ui/controls/icons';
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg, TooltipTextSvg, TooltipTextOutlineSvg, PlusBoxSvg, MinusBoxSvg } from '../../../mol-plugin-ui/controls/icons';
import { PluginCommands } from '../../../mol-plugin/commands';
import { State, StateObjectCell, StateSelection, StateTransformer } from '../../../mol-state';
import { ParameterControls, ParameterMappingControl, ParamOnChange, SelectControl } from '../../../mol-plugin-ui/controls/parameters';
@@ -18,14 +18,17 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
import { MarkerAction } from '../../../mol-util/marker-action';
import { EveryLoci, Loci } from '../../../mol-model/loci';
import { deepEqual } from '../../../mol-util';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams } from '../data/state';
import React from 'react';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
import React, { useState } from 'react';
import { MesoscaleExplorerState } from '../app';
import { StructureElement } from '../../../mol-model/structure/structure/element';
import { PluginStateObject as PSO } from '../../../mol-plugin-state/objects';
import { Structure } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { Sphere3D } from '../../../mol-math/geometry';
import { MesoFocusLoci } from '../behavior/camera';
import Markdown from 'react-markdown';
import { combineLatest } from 'rxjs';
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
const { canvas3d } = plugin;
@@ -60,6 +63,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
if (!state.description && !state.link) return;
return {
selectionDescription: state.focusInfo,
description: state.description,
link: state.link,
};
@@ -68,7 +72,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
render() {
const info = this.info;
return info && <>
<div className='msp-help-text'>
<div id='modelinfo' className='msp-help-text'>
<div>{info.description}</div>
<div><a href={info.link} target='_blank'>Source</a></div>
</div>
@@ -101,17 +105,20 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
}
get info() {
const info: { label: string, key: string }[] = [];
const infos: { label: string, key: string, description?: string }[] = [];
this.plugin.managers.structure.selection.entries.forEach((e, k) => {
if (StructureElement.Loci.is(e.selection) && !StructureElement.Loci.isEmpty(e.selection)) {
const cell = this.plugin.helpers.substructureParent.get(e.selection.structure);
info.push({
const { entities } = e.selection.structure.model;
const description = entities.data.pdbx_description.value(0)[0] || 'model';
infos.push({
description: description,
label: cell?.obj?.label || 'Unknown',
key: k,
});
}
});
return info;
return infos;
}
find(label: string) {
@@ -133,31 +140,40 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
const loci = Structure.toStructureElementLoci(e.selection.structure);
centerLoci(this.plugin, loci);
const cell = this.plugin.helpers.substructureParent.get(loci.structure);
const d = getCellDescription(cell!); // '### ' + cell?.obj?.label + '\n\n' + cell?.obj?.description;
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
}
get selection() {
const info = this.info;
const help_selection = <><div>Use <i>ctrl+left</i> to select entities, either on the 3D canvas or in the tree below</div><div>Use <i>shift+left</i> to select individual chain on the 3D canvas</div></>;
if (!info.length) return <>
<div className='msp-help-text'>
<div>Use <i>ctrl+left click</i> to select entities, either on the 3D canvas or in the tree below</div>
<div id='seleinfo' className='msp-help-text'>
{help_selection}
</div>
</>;
return <>
{info.map((entry, index) => {
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
onClick={() => this.center(entry.key)}
>
<span title={entry.label}>{entry.label}</span>
</Button>;
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
return <div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
{label}
{find}
{remove}
</div>;
})}
<div id='seleinfo'>
{info.map((entry, index) => {
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
onClick={() => this.center(entry.key)}
>
<span title={entry.label}>
{entry.label}
</span>
</Button>;
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
return <>
<div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
{label}
{find}
{remove}
</div>
</>;
})};
</div>
</>;
}
@@ -216,7 +232,7 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
renderStyle() {
const style = this.style || '';
return <div style={{ margin: '5px', marginBottom: '10px' }}>
return <div id='selestyle' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Style'} param={SelectionStyleParam} value={style} onChange={(e) => { this.setStyle(e.value); }} />
</div>;
}
@@ -229,6 +245,192 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
}
}
export function MesoMarkdownAnchor({ href, children, element }: { href?: string, children?: any, element?: any }) {
const plugin = React.useContext(PluginReactContext);
if (!href) return element;
// Decode the href to handle encoded spaces and other characters
const decodedHref = href ? decodeURIComponent(href) : '';
const handleHover = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
if (decodedHref.startsWith('i')) {
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const query_names = decodedHref.substring(1).split(',');
for (const query_name of query_names) {
const entities = getEveryEntity(plugin, query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
} else if (decodedHref.startsWith('g')) {
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const qindex = decodedHref.indexOf('.');
const query = decodedHref.substring(1, qindex) + ':';
const query_names = decodedHref.substring(qindex + 1).split(',');
for (const query_name of query_names) {
const e = getAllEntities(plugin, query + query_name);
for (const r of e) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
}
};
const handleLeave = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
// Implement your hover off logic here
// Example: Perform an action if the href starts with 'h'
if (decodedHref.startsWith('i') || decodedHref.startsWith('g')) {
// Example hover off action
e.preventDefault();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
}
};
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
if (href.startsWith('#')) {
plugin.managers.snapshot.applyKey(decodedHref.substring(1));
} else if (decodedHref.startsWith('i')) {
plugin.managers.interactivity.lociSelects.deselectAll();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const query_names = decodedHref.substring(1).split(',');
for (const query_name of query_names) {
const entities = getFilteredEntities(plugin, '', query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) {
return;
}
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
}
}
} else if (decodedHref.startsWith('g')) {
plugin.managers.interactivity.lociSelects.deselectAll();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const qindex = decodedHref.indexOf('.');
const query = decodedHref.substring(1, qindex) + ':';
const query_names = decodedHref.substring(qindex + 1).split(',');
for (const query_name of query_names) {
const entities = getAllEntities(plugin, query + query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) return;
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
}
}
} else {
// open the link in a new tab
window.open(decodedHref, '_blank');
}
};
if (decodedHref[0] === '#') {
return <a href={decodedHref[0]} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
}
if (decodedHref[0] === 'i' || decodedHref[0] === 'g') {
return <a href={decodedHref[0]} onMouseLeave={handleLeave} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
}
if (decodedHref[0] === 'h') {
return <a href={decodedHref[0]} onClick={handleClick} rel='noopener noreferrer'>{children}</a>;
}
return element;
}
export function MesoViewportSnapshotDescription() {
let tSize = 14;
const plugin = React.useContext(PluginReactContext);
if (MesoscaleState.has(plugin)) {
const state = MesoscaleState.get(plugin);
tSize = state.textSizeDescription;
}
const [_, setV] = React.useState(0);
const [isShown, setIsShown] = useState(true);
const [textSize, setTextSize] = useState(tSize);
const toggleVisibility = () => {
setIsShown(!isShown);
};
const increaseTextSize = () => {
setTextSize(prevSize => Math.min(prevSize + 2, 50)); // Increase the text size by 2px, but not above 50px
};
const decreaseTextSize = () => {
setTextSize(prevSize => Math.max(prevSize - 2, 2)); // Decrease the text size by 2px, but not below 2px
};
React.useEffect(() => {
const sub = plugin.managers.snapshot.events.changed.subscribe(() => setV(v => v + 1));
return () => sub.unsubscribe();
}, [plugin]);
const current = plugin.managers.snapshot.state.current;
if (!current) return null;
const e = plugin.managers.snapshot.getEntry(current)!;
if (!e?.description?.trim()) return null;
if (MesoscaleState.has(plugin)) {
MesoscaleState.set(plugin, { textSizeDescription: textSize });
}
const showInfo = <IconButton svg={isShown ? TooltipTextSvg : TooltipTextOutlineSvg} flex='20px' onClick={toggleVisibility} title={isShown ? 'Hide Description' : 'Show Description'}/>;
const increasePoliceSize = <IconButton svg={PlusBoxSvg} flex='20px' onClick={increaseTextSize} title='Bigger Text' />;
const decreasePoliceSize = <IconButton svg={MinusBoxSvg} flex='20px' onClick={decreaseTextSize} title='Smaller Text' />;
return (
<>
<div id='snapinfoctrl' className="msp-state-snapshot-viewport-controls" style={{ marginRight: '30px' }}>
{showInfo}{increasePoliceSize}{decreasePoliceSize}
</div>
<div id='snapinfo' className={`msp-snapshot-description-me ${isShown ? 'shown' : 'hidden'}`} style={{ fontSize: `${textSize}px` }}>
{<Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>}
</div>
</>
);
}
export class FocusInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
componentDidMount() {
this.subscribe(combineLatest([
this.plugin.state.data.behaviors.isUpdating,
this.plugin.managers.structure.selection.events.changed
]), ([isUpdating]) => {
if (!isUpdating) this.forceUpdate();
});
}
get info() {
let focusInfo = '';
if (MesoscaleState.has(this.plugin)) {
const state = MesoscaleState.get(this.plugin);
if (state.focusInfo) focusInfo = state.focusInfo;
}
return focusInfo;
}
render() {
const focusInfo = this.info;
const description = (focusInfo !== '') ? <Markdown skipHtml components={{ a: MesoMarkdownAnchor }}>{focusInfo}</Markdown> : '';
return <>
<div id='focusinfo' className='msp-help-text'>
{description}
</div>
</>;
}
}
export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean }> {
filterRef = React.createRef<HTMLInputElement>();
prevFilter = '';
@@ -336,7 +538,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
renderGraphics() {
const graphics = this.graphics;
return <div style={{ margin: '5px', marginBottom: '10px' }}>
return <div id='graphicsquality' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Graphics'} param={MesoscaleStateParams.graphics} value={`${graphics}`} onChange={(e) => { this.setGraphics(e.value); }} />
</div>;
}
@@ -363,7 +565,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
return <>
{this.renderGraphics()}
<div className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
<div id='searchtree' className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
<input type='text' ref={this.filterRef}
value={filter}
placeholder='Search'
@@ -373,10 +575,12 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
/>
<IconButton svg={CloseSvg} toggleState={false} disabled={disabled} onClick={() => this.setFilter('')} />
</div>
{options.length > 1 && <div style={{ margin: '5px', marginBottom: '10px' }}>
{options.length > 1 && <div id='grouptree' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Group By'} param={groupParam} value={`${groupBy}`} onChange={(e) => { this.setGroupBy(parseInt(e.value)); }} />
</div>}
<GroupNode filter={filter} cell={root} depth={0} />
<div id='tree' style={{ position: 'relative', overflowY: 'auto', borderBottom: '1px solid #000', maxHeight: '600px' }}>
<GroupNode filter={filter} cell={root} depth={0} />
</div>
</>;
}
}
@@ -435,6 +639,12 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
this.setState({ action: this.state.action === 'root' ? undefined : 'root' });
};
showInfo = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
const d = getCellDescription(this.cell); // '### ' + this.cell?.obj?.label + '\n\n' + this.cell?.obj?.description;
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
};
highlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
@@ -493,7 +703,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
updateColor = (values: ColorProps) => {
const update = this.plugin.state.data.build();
const { value, type, lightness, alpha, emissive } = values;
const { value, illustrative, type, lightness, alpha, emissive } = values;
const entities = this.filteredEntities;
@@ -507,8 +717,11 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const c = type === 'generate' ? groupColors[i] : value;
update.to(entities[i]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
@@ -655,6 +868,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={disabled}
onMouseEnter={this.highlight}
onMouseLeave={this.clearHighlight}
onClick={this.showInfo}
>
<span title={groupLabel}>{groupLabel}</span>
</Button>;
@@ -774,18 +988,63 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
if (e.ctrlKey) {
this.toggleSelect(e);
} else {
this.center(e);
const d = getEntityDescription(this.plugin, this.cell);
if (this.cell?.obj?.data.sourceData.state.models.length !== 0) {
const repr = this.cell?.obj?.data.repr;
if (repr) {
// for fiber need to think how to handle.
const aloci = repr.getAllLoci()[0];
const locis = Loci.normalize(aloci, 'chainInstances') as StructureElement.Loci;
const nChain = aloci.structure.state.unitSymmetryGroups.length;
let index = MesoscaleState.get(this.plugin).index + 1;
if (index * nChain >= locis.elements.length) index = 0;
const elems = locis.elements.slice(index * nChain, ((index + 1) * nChain)); // end index is not included
const loci = StructureElement.Loci(aloci.structure, elems);
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const state = this.plugin.state.behaviors;
const selections = state.select(StateSelection.Generators.ofTransformer(MesoFocusLoci));
const params = selections.length === 1 ? selections[0].obj?.data.params : undefined;
if (!params.centerOnly) {
this.plugin.managers.camera.focusSphere(sphere, params);
} else {
const snapshot = this.plugin.canvas3d?.camera.getCenter(sphere.center);
this.plugin.canvas3d?.requestCameraReset({ durationMs: params.durationMs, snapshot });
}
MesoscaleState.set(this.plugin, { index: index, focusInfo: `${d}` });
}
} else {
this.center(e);
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
}
}
};
get colorValue(): Color | undefined {
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
if (this.cell.transform.params?.colorTheme?.params.value) {
return this.cell.transform.params?.colorTheme?.params.value;
} else if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
return this.cell.transform.params?.colorTheme?.params.style.params.value;
} else {
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
}
}
get illustrativeValue(): { illustrative: boolean } | undefined {
return {
illustrative: (this.cell.transform.params?.colorTheme?.name === 'illustrative')
};
}
get lightnessValue(): { lightness: number } | undefined {
return {
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
};
if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
return {
lightness: this.cell.transform.params?.colorTheme?.params.style.params.lightness ?? 0
};
} else {
return {
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
};
}
}
get opacityValue(): { alpha: number } | undefined {
@@ -838,7 +1097,11 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
}
update.to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.value = value;
if (old.colorTheme.name === 'illustrative') {
old.colorTheme.params.style.params.value = value;
} else {
old.colorTheme.params.value = value;
}
} else if (old.coloring) {
old.coloring.params.color = value;
}
@@ -846,10 +1109,26 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
update.commit();
};
updateIllustrative = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.colorTheme) {
if (old.colorTheme.name !== 'illustrative' && values.illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: old.colorTheme.params.value, lightness: old.colorTheme.params.lightness } } } };
} else if (old.colorTheme.name === 'illustrative' && !values.illustrative) {
old.colorTheme = { name: 'uniform', params: { value: old.colorTheme.params.style.params.value, lightness: old.colorTheme.params.style.params.lightness } };
}
}
}).commit();
};
updateLightness = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.lightness = values.lightness;
if (old.colorTheme.name === 'illustrative') {
old.colorTheme.params.style.params.lightness = values.lightness;
} else {
old.colorTheme.params.lightness = values.lightness;
}
} else if (old.coloring) {
old.coloring.params.lightness = values.lightness;
}
@@ -924,6 +1203,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
const depth = this.props.depth;
const colorValue = this.colorValue;
const lightnessValue = this.lightnessValue;
const illustrativeValue = this.illustrativeValue;
const opacityValue = this.opacityValue;
const emissiveValue = this.emissiveValue;
const lodValue = this.lodValue;
@@ -953,6 +1233,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<CombinedColorControl param={ColorValueParam} value={colorValue ?? Color(0xFFFFFF)} onChange={this.updateColor} name='color' hideNameRow />
<ParameterControls params={IllustrativeParams} values={illustrativeValue} onChangeValues={this.updateIllustrative} />
<ParameterControls params={LightnessParams} values={lightnessValue} onChangeValues={this.updateLightness} />
<ParameterControls params={OpacityParams} values={opacityValue} onChangeValues={this.updateOpacity} />
<ParameterControls params={EmissiveParams} values={emissiveValue} onChangeValues={this.updateEmissive} />

View File

@@ -5,20 +5,63 @@
*/
import { Mp4EncoderUI } from '../../../extensions/mp4-export/ui';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { CollapsableControls, CollapsableState, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { SectionHeader } from '../../../mol-plugin-ui/controls/common';
import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
import { PluginCommands } from '../../../mol-plugin/commands';
import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/measurements';
import { MesoscaleExplorerState } from '../app';
import { MesoscaleState } from '../data/state';
import { EntityControls, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls } from './states';
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
import { RendererParams } from '../../../mol-gl/renderer';
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
const Spacer = () => <div style={{ height: '2em' }} />;
const ViewportParams = {
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
};
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
protected defaultState(): CollapsableState {
return {
header: 'Viewport Settings',
isCollapsed: true,
brand: { accent: 'cyan', svg: TuneSvg }
};
}
protected renderControls(): JSX.Element | null {
return <>
{this.plugin.canvas3d && this.plugin.canvas3dContext && <>
<ParameterControls params={ViewportParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
</>}
</>;
}
private setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
};
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
}
}
export class LeftPanel extends PluginUIComponent {
render() {
const customState = this.plugin.customState as MesoscaleExplorerState;
return <div className='msp-scrollable-container'>
{customState.driver && <>
<ExplorerInfo />
<Spacer />
</>}
<SectionHeader title='Database' />
<DatabaseControls />
<Spacer />
@@ -42,6 +85,7 @@ export class LeftPanel extends PluginUIComponent {
<Spacer />
<Mp4EncoderUI />
<ViewportSettingsUI />
</div>;
}
}
@@ -59,6 +103,13 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
);
}
get hasFocusInfo() {
return (
MesoscaleState.has(this.plugin) &&
!!(MesoscaleState.get(this.plugin).focusInfo !== '')
);
}
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
@@ -89,10 +140,18 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
<SectionHeader title='Selection' />
<SelectionInfo />
<Spacer />
<StructureMeasurementsControls initiallyCollapsed={true}/>
</>
<MesoQuickStylesControls />
<Spacer />
<SectionHeader title='Entities' />
<EntityControls />
<Spacer />
{this.hasFocusInfo && <>
<SectionHeader title='Focus Info' />
<FocusInfo />
<Spacer />
</>}
</div>;
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,9 +7,9 @@
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ExpandGroup } from '../../../mol-plugin-ui/controls/common';
import { GetAppSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
import { PluginCommands } from '../../../mol-plugin/commands';
@@ -24,9 +24,13 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
import { createGenericHierarchy } from '../data/generic/preset';
import { createMmcifHierarchy } from '../data/mmcif/preset';
import { createPetworldHierarchy } from '../data/petworld/preset';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps } from '../data/state';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
import { isTimingMode } from '../../../mol-util/debug';
import { now } from '../../../mol-util/now';
function adjustPluginProps(ctx: PluginContext) {
const customState = ctx.customState as MesoscaleExplorerState;
ctx.managers.interactivity.setProps({ granularity: 'chain' });
ctx.canvas3d?.setProps({
multiSample: { mode: 'off' },
@@ -77,6 +81,7 @@ function adjustPluginProps(ctx: PluginContext) {
radius: 5,
bias: 1,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
@@ -84,7 +89,6 @@ function adjustPluginProps(ctx: PluginContext) {
shadow: {
name: 'on',
params: {
bias: 0.6,
maxDistance: 80,
steps: 3,
tolerance: 1.0,
@@ -98,8 +102,13 @@ function adjustPluginProps(ctx: PluginContext) {
color: Color(0x000000),
includeTransparent: false,
}
}
}
},
},
illumination: {
enabled: customState.illumination,
firstStepSize: 0.1,
rayDistance: 1024,
},
});
const { graphics } = MesoscaleState.get(ctx);
@@ -162,14 +171,36 @@ export async function loadExampleEntry(ctx: PluginContext, entry: ExampleEntry)
}
export async function loadUrl(ctx: PluginContext, url: string, type: 'molx' | 'molj' | 'cif' | 'bcif') {
let startTime = 0;
if (isTimingMode) {
startTime = now();
}
if (type === 'molx' || type === 'molj') {
const customState = ctx.customState as MesoscaleExplorerState;
delete customState.stateRef;
customState.stateCache = {};
ctx.managers.asset.clear();
await PluginCommands.State.Snapshots.Clear(ctx);
await PluginCommands.State.Snapshots.OpenUrl(ctx, { url, type });
const cell = ctx.state.data.selectQ(q => q.ofType(MesoscaleStateObject))[0];
if (!cell) throw new Error('Missing MesoscaleState');
customState.stateRef = cell.transform.ref;
customState.graphicsMode = cell.obj?.data.graphics || customState.graphicsMode;
} else {
await reset(ctx);
const isBinary = type === 'bcif';
const data = await ctx.builders.data.download({ url, isBinary });
await createHierarchy(ctx, data.ref);
}
if (isTimingMode) {
const endTime = now();
// Calculate the elapsed time
const timeTaken = endTime - startTime;
console.log(`Model loaded in ${timeTaken} milliseconds`);
}
}
export async function loadPdb(ctx: PluginContext, id: string) {
@@ -269,7 +300,7 @@ export class DatabaseControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='database' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadDatabase} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -281,7 +312,7 @@ export class LoaderControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='loader' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadModel} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -293,7 +324,7 @@ export class ExampleControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='example' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadExample} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -330,7 +361,7 @@ export class SessionControls extends PluginUIComponent {
};
render() {
return <div style={{ margin: '5px' }}>
return <div id='session' style={{ margin: '5px' }}>
<div className='msp-flex-row'>
<Button icon={GetAppSvg} onClick={this.downloadToFileZip} title='Download the state.'>
Download
@@ -346,14 +377,14 @@ export class SessionControls extends PluginUIComponent {
export class SnapshotControls extends PluginUIComponent<{}> {
render() {
return <div style={{ margin: '5px' }}>
<div style={{ marginBottom: '10px' }}>
<div id='snaplist' style={{ marginBottom: '10px' }}>
<LocalStateSnapshotList />
</div>
<div style={{ marginBottom: '10px' }}>
<div id='snap' style={{ marginBottom: '10px' }}>
<LocalStateSnapshots />
</div>
<div style={{ marginBottom: '10px' }}>
<div id='snapoption' style={{ marginBottom: '10px' }}>
<ExpandGroup header='Snapshot Options' initiallyExpanded={false}>
<LocalStateSnapshotParams />
</ExpandGroup>
@@ -361,3 +392,336 @@ export class SnapshotControls extends PluginUIComponent<{}> {
</div>;
}
}
export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, showHelp: boolean }> {
state = {
isDisabled: false,
showHelp: false
};
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref) {
this.forceUpdate();
}
});
}
setupDriver = () => {
// setup the tour of the interface
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver) return;
driver.setSteps([
// Left panel
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
{ element: '#snaplist', popover: { title: 'Snapshot List', description: 'View and manage the list of snapshots. You can reorder them and edit their titles, keys, and descriptions. Snapshot states cannot be edited.', side: 'right', align: 'start' } },
{ element: '#snap', popover: { title: 'Add Snapshot', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
{ element: '#snapoption', popover: { title: 'Snapshot Options', description: 'These options are saved in the snapshot. Set them before adding a snapshot to see their effect during animation playback.', side: 'right', align: 'start' } },
{ element: '#exportanimation', popover: { title: 'Export Animation', description: 'Create movies or scenes with rocking, rotating, or snapshots animations.', side: 'right', align: 'start' } },
{ element: '#viewportsettings', popover: { title: 'Viewport Settings', description: 'Advanced settings for the renderer and trackball.', side: 'right', align: 'start' } },
// Viewport
{ element: '#snapinfo', popover: { title: 'Snapshot Description', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
{ element: '#snapinfoctrl', popover: { title: 'Snapshot Description Control', description: 'Control the visibility and text size of the snapshot description widget.', side: 'right', align: 'start' } },
// Right panel
{ element: '#modelinfo', popover: { title: 'Model Information', description: 'Summary information about the model, if available.', side: 'right', align: 'start' } },
{ element: '#selestyle', popover: { title: 'Selection Style', description: 'Choose the rendering style for entity selection accessed via Shift/Ctrl mouse. Options include: Color & Outline, Color, Outline.', side: 'right', align: 'start' } },
{ element: '#seleinfo', popover: { title: 'Selection List', description: 'View the current list of selected entities.', side: 'right', align: 'start' } },
{ element: '#measurements', popover: { title: 'Measurements', description: 'Use this widget to create labels, measure distances, angles, dihedral orientations, and planes for the selected entities.', side: 'right', align: 'start' } },
{ element: '#quickstyles', popover: { title: 'Quick Styles', description: 'Change between a selection of style presets.', side: 'right', align: 'start' } },
{ element: '#graphicsquality', popover: { title: 'Graphics Quality', description: 'Adjust the overall graphics quality. Lower quality improves performance. Options are: Ultra, Quality (Default), Balanced, Performance, Custom. Custom settings use the Culling & LOD values set in the Tree.', side: 'right', align: 'start' } },
{ element: '#searchtree', popover: { title: 'Search', description: 'Filter the entity tree based on your queries.', side: 'right', align: 'start' } },
{ element: '#grouptree', popover: { title: 'Group By', description: 'Change the grouping of the hierarchy tree, e.g., group by instance or by compartment.', side: 'right', align: 'start' } },
{ element: '#tree', popover: { title: 'Tree Hierarchy', description: 'View the hierarchical tree of entity types in the model.', side: 'right', align: 'start' } },
{ element: '#focusinfo', popover: { title: 'Selection Description', description: 'Detailed information about the current selection, if present in the loaded file.', side: 'right', align: 'start' } },
{ popover: { title: 'Happy Exploring!', description: 'Thats all! Go ahead and start exploring or creating mesoscale tours.' } }
]);
driver.refresh();
};
openHelp = () => {
// open a new page with the documentation
window.open('https://molstar.org/me-docs/', '_blank');
};
toggleHelp = () => {
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver || !driver.hasNextStep()) {
this.setupDriver();
}
this.setState({ showHelp: !this.state.showHelp }, () => {
if (this.state.showHelp && driver) {
driver.drive(); // start at 0
}
});
};
render() {
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver) return;
const help = <IconButton svg={HelpOutlineSvg} toggleState={false} small onClick={this.openHelp} title='Open the Documentation' />;
const tour = <IconButton svg={TourSvg} toggleState={false} small onClick={this.toggleHelp} title='Start the interactive tour' />;
return <>
<div id='explorerinfo' style={{ display: 'flex', alignItems: 'center', padding: '4px 0 4px 8px' }} className='msp-help-text'>
<h2 style={{ flexGrow: 1 }}>Mol* Mesoscale Explorer</h2>
{tour}{help}
</div>
</>;
}
}
export class MesoQuickStylesControls extends CollapsableControls {
defaultState() {
return {
isCollapsed: true,
header: 'Quick Styles',
brand: { accent: 'gray' as const, svg: MagicWandSvg }
};
}
renderControls() {
return <>
<MesoQuickStyles />
</>;
}
}
export class MesoQuickStyles extends PluginUIComponent {
async default() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
...p.postprocessing,
shadow: {
name: 'on',
params: {
maxDistance: 80,
steps: 3,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
await updateStyle(this.plugin, {
ignoreLight: true,
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
celShaded: false,
illustrative: false,
});
}
async celshading() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
...p.postprocessing,
shadow: {
name: 'on',
params: {
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: { name: 'off', params: {} },
dof: { name: 'off', params: {} },
}
});
await updateStyle(this.plugin, {
ignoreLight: false,
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
celShaded: true,
illustrative: false,
});
}
async shinyDof() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
...p.postprocessing,
shadow: {
name: 'on',
params: {
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: { name: 'off', params: {} },
dof: {
name: 'on',
params: {
blurSize: 9,
blurSpread: 1.0,
inFocus: 0.0,
PPM: 200.0,
center: 'camera-target',
mode: 'sphere',
}
}
}
});
await updateStyle(this.plugin, {
ignoreLight: false,
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
celShaded: false,
illustrative: false,
});
}
async illustrative() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
...p.postprocessing,
shadow: {
name: 'on',
params: {
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
await updateStyle(this.plugin, {
ignoreLight: true,
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
celShaded: false,
illustrative: true,
});
}
async shiny() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
...p.postprocessing,
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
dof: { name: 'off', params: {} },
}
});
await updateStyle(this.plugin, {
ignoreLight: false,
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
celShaded: false,
illustrative: false,
});
}
async stylized() {
if (!this.plugin.canvas3d) return;
const p = this.plugin.canvas3d.props;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
...p.postprocessing,
shadow: {
name: 'on',
params: {
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
await updateStyle(this.plugin, {
ignoreLight: false,
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
celShaded: false,
illustrative: true,
});
}
render() {
return <>
<div className='msp-flex-row'>
<Button noOverflow title='Applies default representation preset and sets outline and occlusion effects to default' onClick={() => this.default()} style={{ width: 'auto' }}>
Default
</Button>
<Button noOverflow title='Applies celShading' onClick={() => this.celshading()} style={{ width: 'auto' }}>
Cel-shaded
</Button>
<Button noOverflow title='Applies illustrative colors preset' onClick={() => this.illustrative()} style={{ width: 'auto' }}>
Illustrative
</Button>
</div>
<div className='msp-flex-row'>
<Button noOverflow title='Apply shiny material to default' onClick={() => this.shiny()} style={{ width: 'auto' }}>
Shiny
</Button>
<Button noOverflow title='Enable shiny material, outline, and illustrative colors' onClick={() => this.stylized()} style={{ width: 'auto' }}>
Shiny-Illustrative
</Button>
<Button noOverflow title='Enable DOF and shiny material' onClick={() => this.shinyDof()} style={{ width: 'auto' }}>
Shiny-DOF
</Button>
</div>
</>;
}
}

View File

@@ -3,6 +3,7 @@
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Neli Fonseca <neli@ebi.ac.uk>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
@@ -104,6 +105,7 @@ const DefaultViewerOptions = {
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
illumination: false,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -216,6 +218,7 @@ export class Viewer {
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
}
});
plugin.canvas3d?.setProps({ illumination: { enabled: o.illumination } });
return new Viewer(plugin);
}
@@ -317,7 +320,10 @@ export class Viewer {
source: {
name: 'alphafolddb' as const,
params: {
id: afdb,
provider: {
id: afdb,
encoding: 'bcif'
},
options: {
...params.source.params.options,
representation: 'preset-structure-representation-ma-quality-assessment-plddt'
@@ -423,6 +429,34 @@ export class Viewer {
});
}
loadFullResolutionEMDBMap(emdbId: string, options: { isoValue: Volume.IsoValue, color?: Color }) {
const plugin = this.plugin;
const numericId = parseInt(emdbId.toUpperCase().replace('EMD-', ''));
const url = `https://ftp.ebi.ac.uk/pub/databases/emdb/structures/EMD-${numericId}/map/emd_${numericId}.map.gz`;
return plugin.dataTransaction(async () => {
const data = await plugin.build().toRoot()
.apply(StateTransforms.Data.Download, { url, isBinary: true, label: emdbId }, { state: { isGhost: true } })
.apply(StateTransforms.Data.DeflateData)
.commit();
const parsed = await plugin.dataFormats.get('ccp4')!.parse(plugin, data, { entryId: emdbId });
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
await plugin.build()
.to(volume)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
type: 'isosurface',
typeParams: { alpha: 1, isoValue: options.isoValue },
color: 'uniform',
colorParams: { value: options.color ?? Color(0x33BB33) }
}))
.commit();
});
}
/**
* @example
* viewer.loadTrajectory({

View File

@@ -63,6 +63,7 @@
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
var illumination = getParam('illumination', '[^&]+').trim() === '1';
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
@@ -83,6 +84,7 @@
preferWebgl1: preferWebgl1,
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
illumination: illumination
}).then(viewer => {
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);

View File

@@ -234,13 +234,19 @@ const FORCE_INT_FIELDS = [
'_atom_site.id',
'_atom_site.auth_seq_id',
'_atom_site_anisotrop.id',
'_atom_site_anisotrop.pdbx_auth_seq_id',
'_pdbx_struct_mod_residue.auth_seq_id',
'_pdbx_unobs_or_zero_occ_residues.auth_seq_id',
'_struct_conf.beg_auth_seq_id',
'_struct_conf.end_auth_seq_id',
'_struct_conn.ptnr1_auth_seq_id',
'_struct_conn.ptnr2_auth_seq_id',
'_struct_sheet_range.beg_auth_seq_id',
'_struct_sheet_range.end_auth_seq_id',
'_struct_site.pdbx_auth_seq_id',
'_struct_site_gen.auth_seq_id',
'_struct_mon_prot_cis.auth_seq_id',
'_struct_mon_prot_cis.pdbx_auth_seq_id_2',
];
/**

View File

@@ -13,8 +13,8 @@ import { UniqueArray } from '../../mol-data/generic';
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
const MARTINI_LIPIDS_PATH = path.resolve(LIPIDS_DIR, 'martini_lipids.itp');
const MARTINI_LIPIDS_URL = 'http://www.cgmartini.nl/images/parameters/lipids/Collections/martini_v2.0_lipids_all_201506.itp';
const MARTINI_LIPIDS_PATH = path.resolve(LIPIDS_DIR, 'martini_lipids_v3.itp');
const MARTINI_LIPIDS_URL = 'https://cgmartini-library.s3.ca-central-1.amazonaws.com/1_Downloads/ff_parameters/martini3/martini_v3.0.0_phospholipids_v1.itp';
async function ensureAvailable(path: string, url: string) {
if (FORCE_DOWNLOAD || !fs.existsSync(path)) {
@@ -32,6 +32,7 @@ async function ensureAvailable(path: string, url: string) {
async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PATH, MARTINI_LIPIDS_URL); }
const extraLipids = ['DMPC'];
const v2lipids = ['DAPC', 'DBPC', 'DFPC', 'DGPC', 'DIPC', 'DLPC', 'DNPC', 'DOPC', 'DPPC', 'DRPC', 'DTPC', 'DVPC', 'DXPC', 'DYPC', 'LPPC', 'PAPC', 'PEPC', 'PGPC', 'PIPC', 'POPC', 'PRPC', 'PUPC', 'DAPE', 'DBPE', 'DFPE', 'DGPE', 'DIPE', 'DLPE', 'DNPE', 'DOPE', 'DPPE', 'DRPE', 'DTPE', 'DUPE', 'DVPE', 'DXPE', 'DYPE', 'LPPE', 'PAPE', 'PGPE', 'PIPE', 'POPE', 'PQPE', 'PRPE', 'PUPE', 'DAPS', 'DBPS', 'DFPS', 'DGPS', 'DIPS', 'DLPS', 'DNPS', 'DOPS', 'DPPS', 'DRPS', 'DTPS', 'DUPS', 'DVPS', 'DXPS', 'DYPS', 'LPPS', 'PAPS', 'PGPS', 'PIPS', 'POPS', 'PQPS', 'PRPS', 'PUPS', 'DAPG', 'DBPG', 'DFPG', 'DGPG', 'DIPG', 'DLPG', 'DNPG', 'DOPG', 'DPPG', 'DRPG', 'DTPG', 'DVPG', 'DXPG', 'DYPG', 'LPPG', 'PAPG', 'PGPG', 'PIPG', 'POPG', 'PRPG', 'DAPA', 'DBPA', 'DFPA', 'DGPA', 'DIPA', 'DLPA', 'DNPA', 'DOPA', 'DPPA', 'DRPA', 'DTPA', 'DVPA', 'DXPA', 'DYPA', 'LPPA', 'PAPA', 'PGPA', 'PIPA', 'POPA', 'PRPA', 'PUPA', 'DPP', 'DPPI', 'PAPI', 'PIPI', 'POP', 'POPI', 'PUPI', 'PVP', 'PVPI', 'PADG', 'PIDG', 'PODG', 'PUDG', 'PVDG', 'APC', 'CPC', 'IPC', 'LPC', 'OPC', 'PPC', 'TPC', 'UPC', 'VPC', 'BNSM', 'DBSM', 'DPSM', 'DXSM', 'PGSM', 'PNSM', 'POSM', 'PVSM', 'XNSM', 'DPCE', 'DXCE', 'PNCE', 'XNCE'];
async function run(out: string) {
await ensureLipidsAvailable();
@@ -50,11 +51,15 @@ async function run(out: string) {
UniqueArray.add(lipids, v, v);
}
for (const v of v2lipids) {
UniqueArray.add(lipids, v, v);
}
const lipidNames = JSON.stringify(lipids.array);
if (out) {
const output = `/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
*

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -33,6 +33,7 @@ const Canvas3DPresets = {
radius: 5,
bias: 0.8,
blurKernelSize: 15,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}

View File

@@ -182,7 +182,7 @@ export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
return Task.create('Orbitals Representation 3D', async ctx => {
const params = volumeParams(plugin, a, srcParams);
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
const provider = plugin.representation.volume.registry.get(params.type.name);
if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
const props = params.type.params || {};

View File

@@ -118,7 +118,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Membrane Orientation', async ctx => {
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
const repr = MembraneOrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => MembraneOrientationParams);
await repr.createOrUpdate(params, a.data).runInContext(ctx);
return new PluginStateObject.Shape.Representation3D({ repr, sourceData: a.data }, { label: 'Membrane Orientation' });
@@ -126,7 +126,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
},
update({ a, b, newParams }, plugin: PluginContext) {
return Task.create('Membrane Orientation', async ctx => {
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
const props = { ...b.data.repr.props, ...newParams };
await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
b.data.sourceData = a.data;
@@ -155,7 +155,7 @@ export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
if (!MembraneOrientationProvider.get(structure).value) {
await plugin.runTask(Task.create('Membrane Orientation', async runtime => {
await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure);
await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure);
}));
}

View File

@@ -82,7 +82,7 @@ export const InitAssemblySymmetry3D = StateAction.build({
params: (a, plugin: PluginContext) => getConfiguredDefaultParams(plugin)
})(({ a, ref, state, params }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
try {
const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
const propCtx = { runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
await AssemblySymmetryDataProvider.attach(propCtx, a.data, params);
const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
const symmetryIndex = assemblySymmetryData ? AssemblySymmetryData.firstNonC1(assemblySymmetryData) : -1;
@@ -118,7 +118,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Assembly Symmetry', async ctx => {
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
return StateObject.Null;
@@ -131,7 +131,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
},
update({ a, b, newParams }, plugin: PluginContext) {
return Task.create('Assembly Symmetry', async ctx => {
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
// this should NOT be StateTransformer.UpdateResult.Null
@@ -176,7 +176,7 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
if (!AssemblySymmetryDataProvider.get(structure).value) {
await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
const propCtx = { runtime, assetManager: plugin.managers.asset };
const propCtx = { runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
const propProps = { serverType: params.serverType, serverUrl: params.serverUrl };
await AssemblySymmetryDataProvider.attach(propCtx, structure, propProps);
const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;

View File

@@ -30,7 +30,7 @@ export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
if (!structureCell || !model) return {};
await plugin.runTask(Task.create('Confal Pyramids', async runtime => {
await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, model);
}));
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);

View File

@@ -30,7 +30,7 @@ export const NtCTubePreset = StructureRepresentationPresetProvider({
if (!structureCell || !model) return {};
await plugin.runTask(Task.create('NtC tube', async runtime => {
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, model);
}));
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Mandar Deshpande <mandar@ebi.ac.uk>
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
@@ -8,7 +8,7 @@
import { QualityAssessment, QualityAssessmentProvider } from '../prop';
import { Location } from '../../../../mol-model/location';
import { Bond, StructureElement, Unit } from '../../../../mol-model/structure';
import { Bond, Model, StructureElement, Unit } from '../../../../mol-model/structure';
import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
import { ThemeDataContext } from '../../../../mol-theme/theme';
import { Color } from '../../../../mol-util/color';
@@ -41,8 +41,13 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
const getColor = (location: StructureElement.Location): Color => {
const { unit, element } = location;
if (!Unit.isAtomic(unit)) return DefaultColor;
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
const score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
let score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
if (typeof score !== 'number') {
score = unit.model.atomicConformation.B_iso_or_equiv.value(element);
}
if (score < 0) {
return DefaultColor;
} else if (score <= 50) {
@@ -74,7 +79,7 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
preferSmoothing: true,
color,
props,
description: 'Assigns residue colors according to the pLDDT Confidence score.',
description: 'Assigns residue colors according to the pLDDT Confidence score. If no Model Archive quality assessment score is available, the B-factor value is used instead.',
legend: ConfidenceColorLegend
};
}
@@ -86,7 +91,7 @@ export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfide
factory: PLDDTConfidenceColorTheme,
getParams: getPLDDTConfidenceColorThemeParams,
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT') || (m.atomicConformation.B_iso_or_equiv.isDefined && !Model.isExperimental(m))),
ensureCustomProperties: {
attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => {
if (data.structure) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -76,7 +76,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
stoppedAnimation = false;
for (let i = 0; i <= N; i++) {
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true, updateControls: true });
const image = params.pass.getImageData(width, height, normalizedViewport);
const image = await params.pass.getImageData(ctx, width, height, normalizedViewport);
encoder.addFrameRgba(image.data);
if (ctx.shouldUpdate) {

View File

@@ -106,6 +106,7 @@ export class MVSAnnotations {
if (!file.ok) throw file.error;
annots[spec.id] = await MVSAnnotation.fromSpec(ctx, spec, file.value);
} catch (err) {
ctx.errorContext?.add('mvs', `Failed to obtain annotation (${err}).\nAnnotation specification source params: ${JSON.stringify(spec.source.params)}`);
console.error(`Failed to obtain annotation (${err}).\nAnnotation specification:`, spec);
annots[spec.id] = MVSAnnotation.createEmpty(spec.schema);
}

View File

@@ -2,11 +2,14 @@
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Aliaksei Chareshneu <chareshneu.tech@gmail.com>
*/
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
import { MolViewSpec } from './behavior';
@@ -30,7 +33,8 @@ import { MVSTreeSchema } from './tree/mvs/mvs-tree';
* If `options.keepCamera`, ignore any camera positioning from the MVS state and keep the current camera position instead.
* If `options.sanityChecks`, run some sanity checks and print potential issues to the console.
* `options.sourceUrl` serves as the base for resolving relative URLs/URIs and may itself be relative to the window URL. */
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, sanityChecks?: boolean, sourceUrl?: string } = {}) {
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
plugin.errorContext.clear('mvs');
try {
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
validateTree(MVSTreeSchema, data.root, 'MVS');
@@ -42,6 +46,18 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: { r
} catch (err) {
plugin.log.error(`${err}`);
throw err;
} finally {
if (!options.doNotReportErrors) {
for (const error of plugin.errorContext.get('mvs')) {
plugin.log.warn(error);
PluginCommands.Toast.Show(plugin, {
title: 'Error',
message: error,
timeoutMs: 10000
});
}
}
plugin.errorContext.clear('mvs');
}
}

View File

@@ -313,7 +313,7 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
if (!structureCell || !structure) return {};
await plugin.runTask(Task.create('Validation Report', async runtime => {
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
}));
const colorTheme = GeometryQualityColorThemeProvider.name as any;
@@ -350,7 +350,7 @@ export const ValidationReportDensityFitPreset = StructureRepresentationPresetPro
if (!structureCell || !structure) return {};
await plugin.runTask(Task.create('Validation Report', async runtime => {
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
}));
const colorTheme = DensityFitColorThemeProvider.name as any;
@@ -374,7 +374,7 @@ export const ValidationReportRandomCoilIndexPreset = StructureRepresentationPres
if (!structureCell || !structure) return {};
await plugin.runTask(Task.create('Validation Report', async runtime => {
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
}));
const colorTheme = RandomCoilIndexColorThemeProvider.name as any;

View File

@@ -28,8 +28,6 @@ interface ICamera {
readonly fogNear: number,
}
const tmpPos1 = Vec3();
const tmpPos2 = Vec3();
const tmpClip = Vec4();
class Camera implements ICamera {
@@ -186,14 +184,11 @@ class Camera implements ICamera {
/** World space pixel size at given `point` */
getPixelSize(point: Vec3) {
// project -> unproject of `point` does not exactly return the same
// to get a sufficiently accurate measure we unproject the original
// clip position in addition to the one shifted by one pixel
this.project(tmpClip, point);
this.unproject(tmpPos1, tmpClip);
tmpClip[0] += 1;
this.unproject(tmpPos2, tmpClip);
return Vec3.distance(tmpPos1, tmpPos2);
const w = tmpClip[3];
const rx = this.viewport.width;
const P00 = this.projection[0];
return (2 / w) / (rx * Math.abs(P00));
}
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 Mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -87,22 +87,48 @@ class CameraTransitionManager {
namespace CameraTransitionManager {
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot) => void
const _rot = Quat.identity();
const _rotUp = Quat.identity();
const _rotDist = Quat.identity();
const _sourcePosition = Vec3();
const _targetPosition = Vec3();
export function defaultTransition(out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot): void {
Camera.copySnapshot(out, target);
// Rotate up
Quat.slerp(_rot, Quat.Identity, Quat.rotationTo(_rot, source.up, target.up), t);
Vec3.transformQuat(out.up, source.up, _rot);
Quat.slerp(_rotUp, Quat.Identity, Quat.rotationTo(_rotUp, source.up, target.up), t);
Vec3.transformQuat(out.up, source.up, _rotUp);
// Lerp target, position & radius
Vec3.lerp(out.target, source.target, target.target, t);
Vec3.lerp(out.position, source.position, target.position, t);
// Interpolate distance
const distSource = Vec3.distance(source.target, source.position);
const distTarget = Vec3.distance(target.target, target.position);
const dist = lerp(distSource, distTarget, t);
// Rotate between source and targer direction
Vec3.sub(_sourcePosition, source.position, source.target);
Vec3.normalize(_sourcePosition, _sourcePosition);
Vec3.sub(_targetPosition, target.position, target.target);
Vec3.normalize(_targetPosition, _targetPosition);
Quat.rotationTo(_rotDist, _sourcePosition, _targetPosition);
Quat.slerp(_rotDist, Quat.Identity, _rotDist, t);
Vec3.transformQuat(_sourcePosition, _sourcePosition, _rotDist);
Vec3.scale(_sourcePosition, _sourcePosition, dist);
Vec3.add(out.position, out.target, _sourcePosition);
// Interpolate radius
out.radius = lerp(source.radius, target.radius, t);
// TODO take change of `clipFar` into account
out.radiusMax = lerp(source.radiusMax, target.radiusMax, t);
// Lerp fov & fog
// Interpolate fov & fog
out.fov = lerp(source.fov, target.fov, t);
out.fog = lerp(source.fog, target.fog, t);
}

View File

@@ -1,12 +1,12 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { BehaviorSubject, Subscription } from 'rxjs';
import { BehaviorSubject, Subject, Subscription, debounceTime, merge } from 'rxjs';
import { now } from '../mol-util/now';
import { Vec3, Vec2 } from '../mol-math/linear-algebra';
import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
@@ -44,6 +44,7 @@ import { degToRad, radToDeg } from '../mol-math/misc';
import { AssetManager } from '../mol-util/assets';
import { deepClone } from '../mol-util/object';
import { HiZParams, HiZPass } from './passes/hi-z';
import { IlluminationParams } from './passes/illumination';
export const Canvas3DParams = {
camera: PD.Group({
@@ -87,11 +88,13 @@ export const Canvas3DParams = {
sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
transparentBackground: PD.Boolean(false),
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'extra pixels to around target to check in case target is empty' }),
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
multiSample: PD.Group(MultiSampleParams),
postprocessing: PD.Group(PostprocessingParams),
marking: PD.Group(MarkingParams),
illumination: PD.Group(IlluminationParams),
hiZ: PD.Group(HiZParams),
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
@@ -119,7 +122,9 @@ interface Canvas3DContext {
readonly contextRestored?: BehaviorSubject<now.Timestamp>
readonly assetManager: AssetManager
readonly changed?: BehaviorSubject<undefined>
readonly pixelScale: number
syncPixelScale(): void
setProps: (props?: Partial<Canvas3DContext.Props>) => void
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
}
@@ -139,6 +144,7 @@ namespace Canvas3DContext {
export type Attribs = typeof DefaultAttribs
export const Params = {
resolutionMode: PD.Select('scaled', PD.arrayToOptions(['scaled', 'native'] as const)),
pixelScale: PD.Numeric(1, { min: 0.1, max: 2, step: 0.05 }),
pickScale: PD.Numeric(0.25, { min: 0.1, max: 1, step: 0.05 }),
transparency: PD.Select('wboit', [['blended', 'Blended'], ['wboit', 'Weighted, Blended'], ['dpoit', 'Depth Peeling']] as const),
@@ -163,7 +169,15 @@ namespace Canvas3DContext {
});
if (gl === null) throw new Error('Could not create a WebGL rendering context');
const { pixelScale, pickScale, transparency } = p;
const getPixelScale = () => p.resolutionMode === 'native' ? p.pixelScale : (p.pixelScale / window?.devicePixelRatio || 1);
const syncPixelScale = () => {
const pixelScale = getPixelScale();
input.setPixelScale(pixelScale);
webgl.setPixelScale(pixelScale);
};
const { pickScale, transparency } = p;
const pixelScale = getPixelScale();
const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
const webgl = createContext(gl, { pixelScale });
const passes = new Passes(webgl, assetManager, { pickScale, transparency });
@@ -224,16 +238,27 @@ namespace Canvas3DContext {
contextRestored: webgl.contextRestored,
assetManager,
changed,
get pixelScale() { return getPixelScale(); },
syncPixelScale,
setProps: (props?: Partial<Props>) => {
if (!props) return;
let hasChanged = false;
let pixelScaleNeedsUpdate = false;
if (props.resolutionMode !== undefined && props.resolutionMode !== p.resolutionMode) {
p.resolutionMode = props.resolutionMode;
pixelScaleNeedsUpdate = true;
}
if (props.pixelScale !== undefined && props.pixelScale !== p.pixelScale) {
p.pixelScale = props.pixelScale;
input.setPixelScale(props.pixelScale);
webgl.setPixelScale(props.pixelScale);
pixelScaleNeedsUpdate = true;
}
if (pixelScaleNeedsUpdate) {
syncPixelScale();
a.handleResize();
hasChanged = true;
}
@@ -346,6 +371,7 @@ namespace Canvas3D {
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
const reprCount = new BehaviorSubject(0);
const interactionEvent = new Subject<void>();
let startTime = now();
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
@@ -443,6 +469,8 @@ namespace Canvas3D {
scene.update(void 0, true);
helper.handle.scene.update(void 0, true);
helper.camera.scene.update(void 0, true);
interactionEvent.next();
}
return changed;
}
@@ -460,6 +488,8 @@ namespace Canvas3D {
return changed;
}
let fenceSync: WebGLSync | null = null;
function render(force: boolean) {
if (webgl.isContextLost) return false;
@@ -474,6 +504,14 @@ namespace Canvas3D {
y > gl.drawingBufferHeight || y + height < 0
) return false;
if (fenceSync !== null) {
if (webgl.checkSyncStatus(fenceSync)) {
fenceSync = null;
} else {
return false;
}
}
const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled);
let didRender = false;
@@ -483,29 +521,55 @@ namespace Canvas3D {
const shouldRender = force || cameraChanged || resized || forceNextRender;
forceNextRender = false;
const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
if (shouldRender || multiSampleChanged || markingUpdated) {
let cam: Camera | StereoCamera = camera;
if (p.camera.stereo.name === 'on') {
stereoCamera.update();
cam = stereoCamera;
if (passes.illumination.supported && p.illumination.enabled) {
if (shouldRender || markingUpdated) {
renderer.setOcclusionTest(null);
passes.illumination.reset();
}
if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
const ctx = { renderer, camera: cam, scene, helper };
if (MultiSamplePass.isEnabled(p.multiSample)) {
const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
multiSampleHelper.render(ctx, p, true, forceOn);
} else {
passes.draw.render(ctx, p, true);
}
hiZ.render(camera);
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
if (passes.illumination.shouldRender(p)
&& ((!isActivelyInteracting && scene.count > 0) || passes.illumination.iteration === 0 || p.userInteractionReleaseMs === 0)
) {
if (isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true });
const ctx = { renderer, camera, scene, helper };
passes.illumination.render(ctx, p, true);
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
// if only marking has updated, do not set the flag to dirty
pickHelper.dirty = pickHelper.dirty || shouldRender;
didRender = true;
// if only marking has updated, do not set the flag to dirty
pickHelper.dirty = pickHelper.dirty || shouldRender;
didRender = true;
}
} else {
const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
if (shouldRender || multiSampleChanged || markingUpdated) {
renderer.setOcclusionTest(hiZ.isOccluded);
let cam: Camera | StereoCamera = camera;
if (p.camera.stereo.name === 'on') {
stereoCamera.update();
cam = stereoCamera;
}
if (isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true });
const ctx = { renderer, camera: cam, scene, helper };
if (MultiSamplePass.isEnabled(p.multiSample)) {
const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
multiSampleHelper.render(ctx, p, true, forceOn);
} else {
passes.draw.render(ctx, p, true);
}
hiZ.render(camera);
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
// if only marking has updated, do not set the flag to dirty
pickHelper.dirty = pickHelper.dirty || shouldRender;
didRender = true;
}
}
if (didRender) {
fenceSync = webgl.getFenceSync();
}
return didRender;
@@ -774,11 +838,13 @@ namespace Canvas3D {
transparentBackground: p.transparentBackground,
dpoitIterations: p.dpoitIterations,
pickPadding: p.pickPadding,
userInteractionReleaseMs: p.userInteractionReleaseMs,
viewport: p.viewport,
postprocessing: { ...p.postprocessing },
marking: { ...p.marking },
multiSample: { ...p.multiSample },
illumination: { ...p.illumination },
hiZ: { ...hiZ.props },
renderer: { ...renderer.props },
trackball: { ...controls.props },
@@ -814,6 +880,36 @@ namespace Canvas3D {
requestDraw();
});
// Monitor user interactions
let isDragging = false;
let isActivelyInteracting = false;
let interactionSubs = [
input.drag.subscribe(() => {
isDragging = true;
}),
input.interactionEnd.subscribe(() => {
isDragging = false;
}),
merge(
input.drag,
input.pinch,
input.wheel,
input.interactionEnd,
).subscribe(() => {
interactionEvent.next();
}),
interactionEvent.subscribe(() => {
isActivelyInteracting = true;
}),
interactionEvent.pipe(
debounceTime(p.userInteractionReleaseMs)
).subscribe(() => {
isActivelyInteracting = isDragging;
if (!isDragging) requestDraw();
}),
];
//
if (isDebugMode && canvas) {
@@ -869,6 +965,7 @@ namespace Canvas3D {
reprRenderObjects.clear();
scene.clear();
helper.debug.clear();
if (fenceSync !== null) webgl.deleteSync(fenceSync);
requestDraw();
reprCount.next(reprRenderObjects.size);
},
@@ -962,6 +1059,7 @@ namespace Canvas3D {
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
if (props.pickPadding !== undefined) p.pickPadding = props.pickPadding;
if (props.userInteractionReleaseMs !== undefined) p.userInteractionReleaseMs = props.userInteractionReleaseMs;
if (props.viewport !== undefined) {
const doNotUpdate = p.viewport === props.viewport ||
(p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
@@ -981,6 +1079,7 @@ namespace Canvas3D {
}
if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
if (props.marking) Object.assign(p.marking, props.marking);
if (props.illumination) Object.assign(p.illumination, props.illumination);
if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
if (props.hiZ) hiZ.setProps(props.hiZ);
if (props.renderer) renderer.setProps(props.renderer);
@@ -1021,6 +1120,10 @@ namespace Canvas3D {
dispose: () => {
contextRestoredSub.unsubscribe();
ctxChangedSub?.unsubscribe();
for (const s of interactionSubs) s.unsubscribe();
interactionSubs = [];
cancelAnimationFrame(animationFrameHandle);
markBuffer = [];
@@ -1031,6 +1134,7 @@ namespace Canvas3D {
renderer.dispose();
interactionHelper.dispose();
hiZ.dispose();
if (fenceSync !== null) webgl.deleteSync(fenceSync);
removeConsoleStatsProvider(consoleStats);
}

View File

@@ -3,6 +3,7 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Herman Bergwerf <post@hbergwerf.nl>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2018 three.js authors. MIT License
@@ -10,7 +11,7 @@
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
import { Viewport } from '../camera/util';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Camera } from '../camera';
import { absMax, degToRad } from '../../mol-math/misc';
@@ -67,7 +68,7 @@ export const TrackballControlsParams = {
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
spin: PD.Group({
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }, { description: 'Rotation speed in radians per second' }),
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
rock: PD.Group({
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
@@ -134,7 +135,6 @@ namespace TrackballControls {
const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
const wheelSub = input.wheel.subscribe(onWheel);
const pinchSub = input.pinch.subscribe(onPinch);
const gestureSub = input.gesture.subscribe(onGesture);
const keyDownSub = input.keyDown.subscribe(onKeyDown);
const keyUpSub = input.keyUp.subscribe(onKeyUp);
const moveSub = input.move.subscribe(onMove);
@@ -614,18 +614,27 @@ namespace TrackballControls {
}
}
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
_isInteracting = true;
function onPinch({ isStart, startX, startY, centerPageX, centerPageY, fractionDelta, buttons, modifiers }: PinchInput) {
if (outsideViewport(startX, startY)) return;
const pan = Binding.match(b.dragPan, buttons, modifiers);
const zoom = Binding.match(b.scrollZoom, buttons, modifiers);
_isInteracting = pan || zoom;
if (pan) {
getMouseOnScreen(centerPageX, centerPageY);
if (isStart) {
Vec2.copy(_panStart, mouseOnScreenVec2);
Vec2.copy(_panEnd, _panStart);
} else {
Vec2.copy(_panEnd, mouseOnScreenVec2);
}
}
if (zoom) {
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
}
}
function onGesture({ deltaScale }: GestureInput) {
_isInteracting = true;
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
}
function onMove({ movementX, movementY }: MoveInput) {
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
@@ -804,7 +813,6 @@ namespace TrackballControls {
dragSub.unsubscribe();
wheelSub.unsubscribe();
pinchSub.unsubscribe();
gestureSub.unsubscribe();
interactionEndSub.unsubscribe();
keyDownSub.unsubscribe();
keyUpSub.unsubscribe();
@@ -817,8 +825,8 @@ namespace TrackballControls {
function spin(deltaT: number) {
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
const frameSpeed = p.animate.params.speed / 1000;
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
const radPerMs = p.animate.params.speed / 1000;
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
}

View File

@@ -310,8 +310,29 @@ export class BackgroundPass {
);
}
render() {
if (!this.isReady()) return;
private readonly bgColor = Vec3();
clear(props: BackgroundProps, transparentBackground: boolean, backgroundColor: Color) {
const { gl, state } = this.webgl;
if (this.isEnabled(props)) {
if (transparentBackground) {
state.clearColor(0, 0, 0, 0);
} else {
Color.toVec3Normalized(this.bgColor, backgroundColor);
state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1);
}
gl.clear(gl.COLOR_BUFFER_BIT);
state.enable(gl.BLEND);
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
} else {
state.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}
render(props: BackgroundProps) {
if (!this.isEnabled(props) || !this.isReady()) return;
if (this.renderable.values.dVariant.ref.value === 'image') {
this.updateImageScaling();

View File

@@ -113,8 +113,9 @@ export class DofPass {
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
if (this.renderable.values.dBlurSize.ref.value !== props.blurSize) {
ValueCell.update(this.renderable.values.dBlurSize, props.blurSize);
const blurSize = Math.round(props.blurSize * this.webgl.pixelRatio);
if (this.renderable.values.dBlurSize.ref.value !== blurSize) {
ValueCell.update(this.renderable.values.dBlurSize, blurSize);
needsUpdate = true;
}

View File

@@ -166,16 +166,19 @@ export class DrawPass {
renderer.renderDpoitOpaque(scene.primitives, camera, null);
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
@@ -220,16 +223,19 @@ export class DrawPass {
renderer.renderWboitOpaque(scene.primitives, camera, null);
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
// render transparent primitives and volumes
@@ -278,6 +284,17 @@ export class DrawPass {
this.colorTarget.bind();
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (!this.packedDepth) {
this.depthTextureOpaque.detachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
@@ -285,15 +302,7 @@ export class DrawPass {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
}
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');

View File

@@ -0,0 +1,667 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { CopyRenderable, QuadSchema, QuadValues, createCopyRenderable } from '../../mol-gl/compute/util';
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
import { Texture } from '../../mol-gl/webgl/texture';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ValueCell } from '../../mol-util';
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
import { Renderer, RendererProps } from '../../mol-gl/renderer';
import { Camera, ICamera } from '../camera';
import { Scene } from '../../mol-gl/scene';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { ShaderCode } from '../../mol-gl/shader-code';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
import { compose_frag } from '../../mol-gl/shader/illumination/compose.frag';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color/color';
import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
import { DrawPass } from './draw';
import { MarkingPass, MarkingProps } from './marking';
import { Helper } from '../helper/helper';
import { DofPass } from './dof';
import { TracingParams, TracingPass } from './tracing';
import { JitterVectors, MultiSampleProps } from './multi-sample';
import { compose_frag as multiSample_compose_frag } from '../../mol-gl/shader/compose.frag';
import { clamp, lerp } from '../../mol-math/interpolate';
type Props = {
transparentBackground: boolean;
dpoitIterations: number;
illumination: IlluminationProps;
renderer: RendererProps;
postprocessing: PostprocessingProps;
marking: MarkingProps;
multiSample: MultiSampleProps;
}
type RenderContext = {
renderer: Renderer;
camera: Camera;
scene: Scene;
helper: Helper;
}
export const IlluminationParams = {
enabled: PD.Boolean(false),
maxIterations: PD.Numeric(5, { min: 0, max: 16, step: 1 }, { description: 'Maximum number of tracing iterations. Final iteration count is 2^x.' }),
denoise: PD.Boolean(true),
denoiseThreshold: PD.Interval([0.15, 1], { min: 0, max: 4, step: 0.01 }, { description: 'Threshold for denoising. Automatically adjusted within given interval based on current iteration.' }),
ignoreOutline: PD.Boolean(true, { description: 'Ignore outline in illumination pass where it is generally not needed for visual clarity. Useful when illumination is often toggled on/off.' }),
...TracingParams,
};
export type IlluminationProps = PD.Values<typeof IlluminationParams>
export class IlluminationPass {
private readonly tracing: TracingPass;
private readonly transparentTarget: RenderTarget;
private readonly depthTargetTransparent: RenderTarget;
private readonly outputTarget: RenderTarget;
readonly packedDepth: boolean;
private readonly copyRenderable: CopyRenderable;
private readonly composeRenderable: ComposeRenderable;
private multiSampleComposeTarget: RenderTarget;
private multiSampleHoldTarget: RenderTarget;
private multiSampleAccumulateTarget: RenderTarget;
private multiSampleCompose: MultiSampleComposeRenderable;
private _iteration = 0;
get iteration() { return this._iteration; }
private _colorTarget: RenderTarget;
get colorTarget() { return this._colorTarget; }
private _supported = false;
get supported() {
return this._supported;
}
getMaxIterations(props: Props) {
return Math.pow(2, props.illumination.maxIterations);
}
static isSupported(webgl: WebGLContext) {
const { drawBuffers, textureFloat, colorBufferFloat, depthTexture } = webgl.extensions;
if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
if (isDebugMode) {
const missing: string[] = [];
if (!textureFloat) missing.push('textureFloat');
if (!colorBufferFloat) missing.push('colorBufferFloat');
if (!depthTexture) missing.push('depthTexture');
if (!drawBuffers) missing.push('drawBuffers');
console.log(`Missing "${missing.join('", "')}" extensions required for "illumination"`);
}
return false;
} else {
return true;
}
}
constructor(private readonly webgl: WebGLContext, private readonly drawPass: DrawPass) {
if (!IlluminationPass.isSupported(webgl)) return;
const { colorTarget } = drawPass;
const width = colorTarget.getWidth();
const height = colorTarget.getHeight();
this.tracing = new TracingPass(webgl, width, height);
this.transparentTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
this.depthTargetTransparent = webgl.createRenderTarget(width, height);
this.outputTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.copyRenderable = createCopyRenderable(webgl, this.transparentTarget.texture);
this.composeRenderable = getComposeRenderable(webgl, this.tracing.accumulateTarget.texture, this.tracing.normalTextureOpaque, this.tracing.colorTextureOpaque, this.tracing.depthTextureOpaque, this.depthTargetTransparent.texture, this.drawPass.postprocessing.outline.target.texture, false);
this.multiSampleComposeTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.multiSampleHoldTarget = webgl.createRenderTarget(width, height, false);
this.multiSampleAccumulateTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.multiSampleCompose = getMultiSampleComposeRenderable(webgl, this.outputTarget.texture);
this._supported = true;
}
private renderInput(renderer: Renderer, camera: ICamera, scene: Scene, props: Props) {
if (isTimingMode) this.webgl.timer.mark('IlluminationPass.renderInput');
const { gl, state } = this.webgl;
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
const hasMarking = markingEnabled && scene.markerAverage > 0;
this.tracing.composeTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
if (hasTransparent) {
if (this.drawPass.transparency === 'wboit') {
this.drawPass.wboit.bind();
renderer.renderWboitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque);
if (scene.volumes.renderables.length > 0) {
renderer.renderWboitTransparent(scene.volumes, camera, this.tracing.depthTextureOpaque);
}
this.tracing.composeTarget.bind();
this.drawPass.wboit.render();
} else if (this.drawPass.transparency === 'dpoit') {
const dpoitTextures = this.drawPass.dpoit.bind();
renderer.renderDpoitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque, dpoitTextures);
for (let i = 0, il = props.dpoitIterations; i < il; i++) {
if (isTimingMode) this.webgl.timer.mark('DpoitPass.layer');
const dpoitTextures = this.drawPass.dpoit.bindDualDepthPeeling();
renderer.renderDpoitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque, dpoitTextures);
this.tracing.composeTarget.bind();
this.drawPass.dpoit.renderBlendBack();
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
}
// evaluate dpoit
this.tracing.composeTarget.bind();
this.drawPass.dpoit.render();
if (scene.volumes.renderables.length > 0) {
renderer.renderDpoitVolume(scene.volumes, camera, this.tracing.depthTextureOpaque);
}
} else {
this.tracing.composeTarget.bind();
this.tracing.depthTextureOpaque.attachFramebuffer(this.tracing.composeTarget.framebuffer, 'depth');
renderer.renderBlendedTransparent(scene.primitives, camera, null);
this.tracing.depthTextureOpaque.detachFramebuffer(this.tracing.composeTarget.framebuffer, 'depth');
if (scene.volumes.renderables.length > 0) {
renderer.renderBlendedVolume(scene.volumes, camera, this.tracing.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(props.postprocessing) && PostprocessingPass.isTransparentOutlineEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
const dofEnabled = DofPass.isEnabled(props.postprocessing);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque);
}
}
}
//
if (hasMarking) {
const markingDepthTest = props.marking.ghostEdgeStrength < 1;
if (markingDepthTest && scene.markerAverage !== 1) {
this.drawPass.marking.depthTarget.bind();
renderer.clear(false, true);
renderer.renderMarkingDepth(scene.primitives, camera, null);
}
this.drawPass.marking.maskTarget.bind();
renderer.clear(false, true);
renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.drawPass.marking.depthTarget.texture : null);
this.drawPass.marking.update(props.marking);
this.drawPass.marking.render(camera.viewport, this.tracing.composeTarget);
}
//
if (antialiasingEnabled) {
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, this.transparentTarget, props.postprocessing);
} else {
if (this.copyRenderable.values.tColor.ref.value !== this.tracing.composeTarget.texture) {
ValueCell.update(this.copyRenderable.values.tColor, this.tracing.composeTarget.texture);
this.copyRenderable.update();
}
this.transparentTarget.bind();
state.enable(gl.SCISSOR_TEST);
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
state.colorMask(true, true, true, true);
state.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
this.copyRenderable.render();
}
this.tracing.composeTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (isTimingMode) this.webgl.timer.markEnd('IlluminationPass.renderInput');
}
shouldRender(props: Props) {
return this._supported && props.illumination.enabled && this._iteration < this.getMaxIterations(props);
}
setSize(width: number, height: number) {
if (!this._supported) return;
const w = this.outputTarget.getWidth();
const h = this.outputTarget.getHeight();
if (width !== w || height !== h) {
this.tracing.setSize(width, height);
this.transparentTarget.setSize(width, height);
this.depthTargetTransparent.setSize(width, height);
this.outputTarget.setSize(width, height);
ValueCell.update(this.copyRenderable.values.uTexSize, Vec2.set(this.copyRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.composeRenderable.values.uTexSize, Vec2.set(this.composeRenderable.values.uTexSize.ref.value, width, height));
this.multiSampleComposeTarget.setSize(width, height);
this.multiSampleHoldTarget.setSize(width, height);
this.multiSampleAccumulateTarget.setSize(width, height);
ValueCell.update(this.multiSampleCompose.values.uTexSize, Vec2.set(this.multiSampleCompose.values.uTexSize.ref.value, width, height));
}
this.drawPass.setSize(width, height);
}
reset(clearAdjustedProps = false) {
if (!this._supported) return;
this.tracing.reset(clearAdjustedProps);
this._iteration = 0;
this.prevSampleIndex = -1;
}
private renderInternal(ctx: RenderContext, props: Props, toDrawingBuffer: boolean, forceRenderInput: boolean) {
if (!this.shouldRender(props)) return;
if (isTimingMode) {
this.webgl.timer.mark('IlluminationPass.render', {
note: `iteration ${this._iteration + 1} of ${this.getMaxIterations(props)}`
});
}
this.tracing.render(ctx, props.transparentBackground, props.illumination, this._iteration, forceRenderInput);
const { renderer, camera, scene, helper } = ctx;
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
if (this._iteration === 0 || forceRenderInput) {
// render color & depth
renderer.setTransparentBackground(props.transparentBackground);
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
renderer.setPixelRatio(this.webgl.pixelRatio);
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
this.renderInput(renderer, camera, scene, props);
}
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.disable(gl.CULL_FACE);
state.depthMask(false);
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.postprocessing.outline.name === 'on' && !props.illumination.ignoreOutline;
let needsUpdateCompose = false;
if (this.composeRenderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
needsUpdateCompose = true;
ValueCell.update(this.composeRenderable.values.dOutlineEnable, outlinesEnabled);
}
if (props.postprocessing.outline.name === 'on') {
const { transparentOutline, outlineScale } = this.drawPass.postprocessing.outline.update(camera, props.postprocessing.outline.params, this.depthTargetTransparent.texture, this.tracing.depthTextureOpaque);
this.drawPass.postprocessing.outline.render();
ValueCell.update(this.composeRenderable.values.uOutlineColor, Color.toVec3Normalized(this.composeRenderable.values.uOutlineColor.ref.value, props.postprocessing.outline.params.color));
if (this.composeRenderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateCompose = true;
ValueCell.update(this.composeRenderable.values.dOutlineScale, outlineScale);
}
if (this.composeRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateCompose = true;
ValueCell.update(this.composeRenderable.values.dTransparentOutline, transparentOutline);
}
}
ValueCell.updateIfChanged(this.composeRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.composeRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.composeRenderable.values.uFogFar, camera.fogFar);
ValueCell.updateIfChanged(this.composeRenderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.composeRenderable.values.uFogColor, Color.toVec3Normalized(this.composeRenderable.values.uFogColor.ref.value, renderer.props.backgroundColor));
if (this.composeRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.composeRenderable.values.dOrthographic, orthographic);
needsUpdateCompose = true;
}
// background
const _toDrawingBuffer = toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing) && props.postprocessing.dof.name === 'off';
if (_toDrawingBuffer) {
this.webgl.unbindFramebuffer();
} else {
this.tracing.composeTarget.bind();
}
this._colorTarget = this.tracing.composeTarget;
this.drawPass.postprocessing.background.update(camera, props.postprocessing.background);
this.drawPass.postprocessing.background.clear(props.postprocessing.background, props.transparentBackground, renderer.props.backgroundColor);
this.drawPass.postprocessing.background.render(props.postprocessing.background);
// compose
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing.background));
if (this.composeRenderable.values.dDenoise.ref.value !== props.illumination.denoise) {
ValueCell.update(this.composeRenderable.values.dDenoise, props.illumination.denoise);
needsUpdateCompose = true;
}
const denoiseThreshold = props.multiSample.mode === 'on'
? props.illumination.denoiseThreshold[0]
: lerp(props.illumination.denoiseThreshold[1], props.illumination.denoiseThreshold[0], clamp(this.iteration / (this.getMaxIterations(props) / 2), 0, 1));
ValueCell.updateIfChanged(this.composeRenderable.values.uDenoiseThreshold, denoiseThreshold);
if (needsUpdateCompose) this.composeRenderable.update();
this.composeRenderable.render();
//
state.enable(gl.BLEND);
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
if (this.copyRenderable.values.tColor.ref.value !== this.transparentTarget.texture) {
ValueCell.update(this.copyRenderable.values.tColor, this.transparentTarget.texture);
this.copyRenderable.update();
}
this.copyRenderable.render();
//
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
renderer.setPixelRatio(this.webgl.pixelRatio);
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
if (helper.debug.isEnabled) {
helper.debug.syncVisibility();
renderer.renderBlended(helper.debug.scene, camera);
}
if (helper.handle.isEnabled) {
renderer.renderBlended(helper.handle.scene, camera);
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}
//
let targetIsDrawingbuffer = false;
let swapTarget = this.outputTarget;
if (AntialiasingPass.isEnabled(props.postprocessing)) {
const _toDrawingBuffer = toDrawingBuffer && props.postprocessing.dof.name === 'off';
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, _toDrawingBuffer ? true : this.outputTarget, props.postprocessing);
if (_toDrawingBuffer) {
targetIsDrawingbuffer = true;
} else {
this._colorTarget = this.outputTarget;
swapTarget = this.tracing.composeTarget;
}
}
if (props.postprocessing.bloom.name === 'on') {
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
this.drawPass.bloom.update(this.tracing.shadedTextureOpaque, this.tracing.normalTextureOpaque, this.tracing.depthTextureOpaque, props.postprocessing.bloom.params);
this.drawPass.bloom.render(camera.viewport, _toDrawingBuffer ? undefined : this._colorTarget);
}
if (props.postprocessing.dof.name === 'on') {
const _toDrawingBuffer = toDrawingBuffer || targetIsDrawingbuffer;
this.drawPass.dof.update(camera, this._colorTarget.texture, this.tracing.depthTextureOpaque, this.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
this.drawPass.dof.render(camera.viewport, _toDrawingBuffer ? undefined : swapTarget);
if (!_toDrawingBuffer) {
this._colorTarget = swapTarget;
}
}
this._iteration += 1;
if (isTimingMode) this.webgl.timer.markEnd('IlluminationPass.render');
this.webgl.gl.flush();
}
private prevSampleIndex = -1;
private renderMultiSample(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
const { camera } = ctx;
const { multiSampleCompose, multiSampleComposeTarget, multiSampleHoldTarget, webgl } = this;
const { gl, state } = webgl;
// based on the Multisample Anti-Aliasing Render Pass
// contributed to three.js by bhouston / http://clara.io/
//
// This manual approach to MSAA re-renders the scene once for
// each sample with camera jitter and accumulates the results.
const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
const maxIterations = this.getMaxIterations(props);
const iteration = Math.min(this._iteration, maxIterations);
const sampleIndex = Math.floor((iteration / maxIterations) * offsetList.length);
if (isTimingMode) {
webgl.timer.mark('IlluminationPass.renderMultiSample', {
note: `sampleIndex ${sampleIndex + 1} of ${offsetList.length}`
});
}
const { x, y, width, height } = camera.viewport;
const sampleWeight = 1.0 / maxIterations;
if (iteration === 0) {
this.renderInternal(ctx, props, false, true);
ValueCell.update(multiSampleCompose.values.uWeight, 1.0);
ValueCell.update(multiSampleCompose.values.tColor, this._colorTarget.texture);
multiSampleCompose.update();
multiSampleHoldTarget.bind();
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
multiSampleCompose.render();
} else {
camera.viewOffset.enabled = true;
ValueCell.update(multiSampleCompose.values.tColor, this._colorTarget.texture);
ValueCell.update(multiSampleCompose.values.uWeight, sampleWeight);
multiSampleCompose.update();
// render the scene multiple times, each slightly jitter offset
// from the last and accumulate the results.
const offset = offsetList[sampleIndex];
Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
camera.update();
// render scene
this.renderInternal(ctx, props, false, this.prevSampleIndex !== sampleIndex);
// compose rendered scene with compose target
multiSampleComposeTarget.bind();
state.enable(gl.BLEND);
state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (iteration === 1) {
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
multiSampleCompose.render();
}
this.prevSampleIndex = sampleIndex;
if (toDrawingBuffer) {
this.webgl.unbindFramebuffer();
} else {
this.multiSampleAccumulateTarget.bind();
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
const accumulationWeight = iteration * sampleWeight;
if (accumulationWeight > 0) {
ValueCell.update(multiSampleCompose.values.uWeight, 1.0);
ValueCell.update(multiSampleCompose.values.tColor, multiSampleComposeTarget.texture);
multiSampleCompose.update();
state.disable(gl.BLEND);
multiSampleCompose.render();
}
if (accumulationWeight < 1.0) {
ValueCell.update(multiSampleCompose.values.uWeight, 1.0 - accumulationWeight);
ValueCell.update(multiSampleCompose.values.tColor, multiSampleHoldTarget.texture);
multiSampleCompose.update();
if (accumulationWeight === 0) state.disable(gl.BLEND);
else state.enable(gl.BLEND);
multiSampleCompose.render();
}
if (!toDrawingBuffer) {
state.disable(gl.BLEND);
this.colorTarget.bind();
if (this.copyRenderable.values.tColor.ref.value !== this.multiSampleAccumulateTarget.texture) {
ValueCell.update(this.copyRenderable.values.tColor, this.multiSampleAccumulateTarget.texture);
this.copyRenderable.update();
}
this.copyRenderable.render();
}
camera.viewOffset.enabled = false;
camera.update();
if (isTimingMode) webgl.timer.markEnd('IlluminationPass.renderMultiSample');
}
render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
if (!this._supported) return;
if (props.multiSample.mode === 'on') {
this.renderMultiSample(ctx, props, toDrawingBuffer);
} else {
this.renderInternal(ctx, props, toDrawingBuffer, false);
}
}
}
//
const ComposeSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dDenoise: DefineSpec('boolean'),
uDenoiseThreshold: UniformSpec('f'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uFogNear: UniformSpec('f'),
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uOutlineColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('b'),
dOutlineEnable: DefineSpec('boolean'),
dOutlineScale: DefineSpec('number'),
dTransparentOutline: DefineSpec('boolean'),
};
const ComposeShaderCode = ShaderCode('compose', quad_vert, compose_frag);
type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, transparentOutline: boolean): ComposeRenderable {
const values: Values<typeof ComposeSchema> = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
tNormal: ValueCell.create(normalTexture),
tShaded: ValueCell.create(shadedTexture),
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tOutlines: ValueCell.create(outlinesTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
dDenoise: ValueCell.create(true),
uDenoiseThreshold: ValueCell.create(0.1),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uFogNear: ValueCell.create(10000),
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
uTransparentBackground: ValueCell.create(false),
dOutlineEnable: ValueCell.create(false),
dOutlineScale: ValueCell.create(1),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...ComposeSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', ComposeShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
//
const MultiSampleComposeSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uWeight: UniformSpec('f'),
};
const MultiSampleComposeShaderCode = ShaderCode('compose', quad_vert, multiSample_compose_frag);
type MultiSampleComposeRenderable = ComputeRenderable<Values<typeof MultiSampleComposeSchema>>
function getMultiSampleComposeRenderable(ctx: WebGLContext, colorTexture: Texture): MultiSampleComposeRenderable {
const values: Values<typeof MultiSampleComposeSchema> = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
uWeight: ValueCell.create(1.0),
};
const schema = { ...MultiSampleComposeSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', MultiSampleComposeShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -1,12 +1,12 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { WebGLContext } from '../../mol-gl/webgl/context';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { Renderer } from '../../mol-gl/renderer';
import { Renderer, RendererParams } from '../../mol-gl/renderer';
import { Scene } from '../../mol-gl/scene';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { DrawPass } from './draw';
@@ -19,6 +19,10 @@ import { Helper } from '../helper/helper';
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
import { MarkingParams } from './marking';
import { AssetManager } from '../../mol-util/assets';
import { IlluminationParams, IlluminationPass } from './illumination';
import { RuntimeContext } from '../../mol-task';
import { isTimingMode } from '../../mol-util/debug';
import { printTimerResults } from '../../mol-gl/webgl/timer';
export const ImageParams = {
transparentBackground: PD.Boolean(false),
@@ -26,8 +30,10 @@ export const ImageParams = {
multiSample: PD.Group(MultiSampleParams),
postprocessing: PD.Group(PostprocessingParams),
marking: PD.Group(MarkingParams),
illumination: PD.Group(IlluminationParams),
cameraHelper: PD.Group(CameraHelperParams),
renderer: PD.Group(RendererParams),
};
export type ImageProps = PD.Values<typeof ImageParams>
@@ -42,6 +48,7 @@ export class ImagePass {
get colorTarget() { return this._colorTarget; }
private readonly drawPass: DrawPass;
private readonly illuminationPass: IlluminationPass;
private readonly multiSamplePass: MultiSamplePass;
private readonly multiSampleHelper: MultiSampleHelper;
private readonly helper: Helper;
@@ -53,6 +60,7 @@ export class ImagePass {
this.props = { ...PD.getDefaultValues(ImageParams), ...props };
this.drawPass = new DrawPass(webgl, assetManager, 128, 128, transparency);
this.illuminationPass = new IlluminationPass(webgl, this.drawPass);
this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
@@ -80,6 +88,7 @@ export class ImagePass {
this._height = height;
this.drawPass.setSize(width, height);
this.illuminationPass.setSize(width, height);
this.multiSamplePass.syncSize();
}
@@ -88,24 +97,59 @@ export class ImagePass {
if (props.cameraHelper) this.helper.camera.setProps(props.cameraHelper);
}
render() {
async render(runtime: RuntimeContext) {
Camera.copySnapshot(this._camera.state, this.camera.state);
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height);
this._camera.update();
const ctx = { renderer: this.renderer, camera: this._camera, scene: this.scene, helper: this.helper };
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
this.multiSampleHelper.render(ctx, this.props, false);
this._colorTarget = this.multiSamplePass.colorTarget;
if (this.illuminationPass.supported && this.props.illumination.enabled) {
await runtime.update({ message: 'Tracing...', current: 1, max: this.illuminationPass.getMaxIterations(this.props) });
this.illuminationPass.reset(true);
while (this.illuminationPass.shouldRender(this.props)) {
if (isTimingMode) this.webgl.timer.mark('ImagePass.render', { captureStats: true });
this.illuminationPass.render(ctx, this.props, false);
if (isTimingMode) this.webgl.timer.markEnd('ImagePass.render');
if (runtime.shouldUpdate) {
await runtime.update({ current: this.illuminationPass.iteration });
}
await this.webgl.waitForGpuCommandsComplete();
}
this._colorTarget = this.illuminationPass.colorTarget;
} else {
this.drawPass.render(ctx, this.props, false);
this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
if (isTimingMode) this.webgl.timer.mark('ImagePass.render', { captureStats: true });
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
this.multiSampleHelper.render(ctx, this.props, false);
this._colorTarget = this.multiSamplePass.colorTarget;
} else {
this.drawPass.render(ctx, this.props, false);
this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
}
if (isTimingMode) this.webgl.timer.markEnd('ImagePass.render');
}
if (isTimingMode) {
const timerResults = this.webgl.timer.resolve();
if (timerResults) {
for (const result of timerResults) {
printTimerResults([result]);
}
}
}
if (isTimingMode) {
const timerResults = this.webgl.timer.resolve();
if (timerResults) {
for (const result of timerResults) {
printTimerResults([result]);
}
}
}
}
getImageData(width: number, height: number, viewport?: Viewport) {
async getImageData(runtime: RuntimeContext, width: number, height: number, viewport?: Viewport) {
this.setSize(width, height);
this.render();
await this.render(runtime);
this.colorTarget.bind();
const w = viewport?.width ?? width, h = viewport?.height ?? height;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -54,6 +54,7 @@ export const MultiSampleParams = {
mode: PD.Select('temporal', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }, { description: 'Take level^2 samples.' }),
reduceFlicker: PD.Boolean(true, { description: 'Reduce flicker in "temporal" mode.' }),
reuseOcclusion: PD.Boolean(true, { description: 'Reuse occlusion data. It is faster but has some artefacts.' }),
};
export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
@@ -161,7 +162,7 @@ export class MultiSamplePass {
ValueCell.update(compose.values.uWeight, sampleWeight);
// render scene
if (i === 0) {
if (i === 0 || !props.multiSample.reuseOcclusion) {
drawPass.postprocessing.setOcclusionOffset(0, 0);
} else {
drawPass.postprocessing.setOcclusionOffset(
@@ -252,7 +253,7 @@ export class MultiSamplePass {
camera.update();
// render scene
if (sampleIndex === 0) {
if (sampleIndex === 0 || !props.multiSample.reuseOcclusion) {
drawPass.postprocessing.setOcclusionOffset(0, 0);
} else {
drawPass.postprocessing.setOcclusionOffset(
@@ -313,7 +314,7 @@ export class MultiSamplePass {
}
}
const JitterVectors = [
export const JitterVectors = [
[
[0, 0]
],

View File

@@ -0,0 +1,147 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
import { isTimingMode } from '../../mol-util/debug';
import { Color } from '../../mol-util/color';
import { PostprocessingProps } from './postprocessing';
export const OutlineParams = {
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
color: PD.Color(Color(0x000000)),
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
};
export type OutlineProps = PD.Values<typeof OutlineParams>
export class OutlinePass {
static isEnabled(props: PostprocessingProps) {
return props.outline.name !== 'off';
}
readonly target: RenderTarget;
private readonly renderable: OutlinesRenderable;
constructor(private readonly webgl: WebGLContext, width: number, height: number, depthTextureTransparent: Texture, depthTextureOpaque: Texture) {
this.target = webgl.createRenderTarget(width, height, false);
this.renderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
}
setSize(width: number, height: number) {
const [w, h] = this.renderable.values.uTexSize.ref.value;
if (width !== w || height !== h) {
this.target.setSize(width, height);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
}
}
update(camera: ICamera, props: OutlineProps, depthTextureTransparent: Texture, depthTextureOpaque: Texture) {
let needsUpdate = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = this.renderable.values.uInvProjection.ref.value;
Mat4.invert(invProjection, camera.projection);
const transparentOutline = props.includeTransparent ?? true;
const outlineScale = Math.max(1, Math.round(props.scale * this.webgl.pixelRatio)) - 1;
const outlineThreshold = 50 * props.threshold * this.webgl.pixelRatio;
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdate = true;
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
}
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdate = true;
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, outlineThreshold);
if (this.renderable.values.tDepthTransparent.ref.value !== depthTextureTransparent) {
needsUpdate = true;
ValueCell.update(this.renderable.values.tDepthTransparent, depthTextureTransparent);
}
if (this.renderable.values.tDepthOpaque.ref.value !== depthTextureOpaque) {
needsUpdate = true;
ValueCell.update(this.renderable.values.tDepthOpaque, depthTextureOpaque);
}
if (needsUpdate) {
this.renderable.update();
}
return { transparentOutline, outlineScale };
}
render() {
if (isTimingMode) this.webgl.timer.mark('OutlinePass.render');
this.target.bind();
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('OutlinePass.render');
}
}
export const OutlinesSchema = {
...QuadSchema,
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
const values: Values<typeof OutlinesSchema> = {
...QuadValues,
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
uTexSize: ValueCell.create(Vec2.create(width, height)),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...OutlinesSchema };
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -9,17 +9,20 @@ import { PickPass } from './pick';
import { MultiSamplePass } from './multi-sample';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { AssetManager } from '../../mol-util/assets';
import { IlluminationPass } from './illumination';
export class Passes {
readonly draw: DrawPass;
readonly pick: PickPass;
readonly multiSample: MultiSamplePass;
readonly illumination: IlluminationPass;
constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, transparency: 'wboit' | 'dpoit' | 'blended' }> = {}) {
const { gl } = webgl;
this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.transparency || 'blended');
this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
this.multiSample = new MultiSamplePass(webgl, this.draw);
this.illumination = new IlluminationPass(webgl, this.draw);
}
setPickScale(pickScale: number) {
@@ -38,5 +41,6 @@ export class Passes {
this.draw.setSize(width, height);
this.pick.syncSize();
this.multiSample.syncSize();
this.illumination.setSize(width, height);
}
}

View File

@@ -300,7 +300,7 @@ export class PickHelper {
}
private render(camera: Camera | StereoCamera) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', true);
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;

View File

@@ -4,27 +4,24 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { deepEqual, ValueCell } from '../../mol-util';
import { ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { DrawPass } from './draw';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
import { Color } from '../../mol-util/color';
import { FxaaParams, FxaaPass } from './fxaa';
import { SmaaParams, SmaaPass } from './smaa';
@@ -32,262 +29,19 @@ import { isTimingMode } from '../../mol-util/debug';
import { BackgroundParams, BackgroundPass } from './background';
import { AssetManager } from '../../mol-util/assets';
import { Light } from '../../mol-gl/renderer';
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
import { CasParams, CasPass } from './cas';
import { DofParams } from './dof';
import { BloomParams } from './bloom';
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
export const OutlinesSchema = {
...QuadSchema,
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
const values: Values<typeof OutlinesSchema> = {
...QuadValues,
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
uTexSize: ValueCell.create(Vec2.create(width, height)),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...OutlinesSchema };
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const ShadowsSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
dSteps: DefineSpec('number'),
uMaxDistance: UniformSpec('f'),
uTolerance: UniformSpec('f'),
uBias: UniformSpec('f'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
};
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
const width = depthTexture.getWidth();
const height = depthTexture.getHeight();
const values: Values<typeof ShadowsSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(width, height)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
dSteps: ValueCell.create(1),
uMaxDistance: ValueCell.create(3.0),
uTolerance: ValueCell.create(1.0),
uBias: ValueCell.create(0.6),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
};
const schema = { ...ShadowsSchema };
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uTexSize: UniformSpec('v2'),
uRadius: UniformSpec('f'),
uBias: UniformSpec('f'),
dMultiScale: DefineSpec('boolean'),
dLevels: DefineSpec('number'),
uLevelRadius: UniformSpec('f[]'),
uLevelBias: UniformSpec('f[]'),
uNearThreshold: UniformSpec('f'),
uFarThreshold: UniformSpec('f'),
};
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
const values: Values<typeof SsaoSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
tDepthHalf: ValueCell.create(depthHalfTexture),
tDepthQuarter: ValueCell.create(depthQuarterTexture),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
uRadius: ValueCell.create(Math.pow(2, 5)),
uBias: ValueCell.create(0.8),
dMultiScale: ValueCell.create(false),
dLevels: ValueCell.create(3),
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
uNearThreshold: ValueCell.create(10.0),
uFarThreshold: ValueCell.create(1500.0),
};
const schema = { ...SsaoSchema };
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoBlurSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uKernel: UniformSpec('f[]'),
dOcclusionKernelSize: DefineSpec('number'),
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
};
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
const values: Values<typeof SsaoBlurSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
uKernel: ValueCell.create(getBlurKernel(15)),
dOcclusionKernelSize: ValueCell.create(15),
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
};
const schema = { ...SsaoBlurSchema };
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
function getBlurKernel(kernelSize: number): number[] {
const sigma = kernelSize / 3.0;
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
const kernel = [];
for (let x = 0; x < halfKernelSize; x++) {
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
}
return kernel;
}
const RandomHemisphereVector: Vec3[] = [];
for (let i = 0; i < 256; i++) {
const v = Vec3();
v[0] = Math.random() * 2.0 - 1.0;
v[1] = Math.random() * 2.0 - 1.0;
v[2] = Math.random();
Vec3.normalize(v, v);
Vec3.scale(v, v, Math.random());
RandomHemisphereVector.push(v);
}
function getSamples(nSamples: number): number[] {
const samples = [];
for (let i = 0; i < nSamples; i++) {
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
scale = 0.1 + scale * (1.0 - 0.1);
samples.push(RandomHemisphereVector[i][0] * scale);
samples.push(RandomHemisphereVector[i][1] * scale);
samples.push(RandomHemisphereVector[i][2] * scale);
}
return samples;
}
const PostprocessingSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
@@ -313,13 +67,12 @@ const PostprocessingSchema = {
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
const values: Values<typeof PostprocessingSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
tColor: ValueCell.create(colorTexture),
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tShadows: ValueCell.create(shadowsTexture),
tOutlines: ValueCell.create(outlinesTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
@@ -353,48 +106,15 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
export const PostprocessingParams = {
occlusion: PD.MappedStatic('on', {
on: PD.Group({
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
multiScale: PD.MappedStatic('off', {
on: PD.Group({
levels: PD.ObjectList({
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
{ radius: 2, bias: 1 },
{ radius: 5, bias: 1 },
{ radius: 8, bias: 1 },
{ radius: 11, bias: 1 },
] }),
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
}),
off: PD.Group({})
}, { cycle: true }),
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
}),
on: PD.Group(SsaoParams),
off: PD.Group({})
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
shadow: PD.MappedStatic('off', {
on: PD.Group({
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
}),
on: PD.Group(ShadowParams),
off: PD.Group({})
}, { cycle: true, description: 'Simplistic shadows' }),
outline: PD.MappedStatic('off', {
on: PD.Group({
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
color: PD.Color(Color(0x000000)),
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
}),
on: PD.Group(OutlineParams),
off: PD.Group({})
}, { cycle: true, description: 'Draw outline around 3D objects' }),
dof: PD.MappedStatic('off', {
@@ -419,381 +139,76 @@ export const PostprocessingParams = {
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
type Levels = {
count: number
radius: number[]
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
bias: (new Array(count * 3)).fill(0),
};
props = props.slice().sort((a, b) => a.radius - b.radius);
for (let i = 0; i < count; ++i) {
const p = props[i];
radius[i] = Math.pow(2, p.radius);
bias[i] = p.bias;
}
return { count, radius, bias };
}
export class PostprocessingPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
}
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on' && props.outline.params.includeTransparent;
return OutlinePass.isEnabled(props) && (props.outline.params as OutlineProps).includeTransparent;
}
readonly target: RenderTarget;
private readonly outlinesTarget: RenderTarget;
private readonly outlinesRenderable: OutlinesRenderable;
private readonly shadowsTarget: RenderTarget;
private readonly shadowsRenderable: ShadowsRenderable;
private readonly ssaoFramebuffer: Framebuffer;
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
private readonly ssaoDepthTexture: Texture;
private readonly ssaoDepthBlurProxyTexture: Texture;
private readonly ssaoRenderable: SsaoRenderable;
private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
private nSamples: number;
private blurKernelSize: number;
private readonly renderable: PostprocessingRenderable;
private ssaoScale: number;
private calcSsaoScale(resolutionScale: number) {
// downscale ssao for high pixel-ratios
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
}
private levels: { radius: number, bias: number }[];
private readonly bgColor = Vec3();
readonly ssao: SsaoPass;
readonly shadow: ShadowPass;
readonly outline: OutlinePass;
readonly background: BackgroundPass;
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) {
const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, readonly drawPass: DrawPass) {
const { colorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
const width = colorTarget.getWidth();
const height = colorTarget.getHeight();
this.nSamples = 1;
this.blurKernelSize = 1;
this.ssaoScale = this.calcSsaoScale(1);
this.levels = [];
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque);
this.shadow = new ShadowPass(webgl, width, height, depthTextureOpaque);
this.outline = new OutlinePass(webgl, width, height, depthTextureTransparent, depthTextureOpaque);
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
this.ssaoFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.downsampledDepthTarget = drawPass.packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'nearest', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
const depthTexture = this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture;
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'nearest', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'nearest', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.ssaoDepthBlurProxyTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, true);
this.background = new BackgroundPass(webgl, assetManager, width, height);
}
setSize(width: number, height: number) {
const [w, h] = this.renderable.values.uTexSize.ref.value;
const ssaoScale = this.calcSsaoScale(1);
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.ssaoScale = ssaoScale;
if (width !== w || height !== h) {
this.target.setSize(width, height);
this.outlinesTarget.setSize(width, height);
this.shadowsTarget.setSize(width, height);
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.depthQuarterTarget.setSize(qw, qh);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
this.depthHalfRenderable.update();
this.ssaoRenderable.update();
this.background.setSize(width, height);
}
this.ssao.setSize(width, height);
this.shadow.setSize(width, height);
this.outline.setSize(width, height);
this.background.setSize(width, height);
}
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
let needsUpdateShadows = false;
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
let needsUpdateMain = false;
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = false;
let needsUpdateOutlines = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.outline.name === 'on';
const shadowsEnabled = props.shadow.name === 'on';
const occlusionEnabled = props.occlusion.name === 'on';
const outlinesEnabled = OutlinePass.isEnabled(props);
const shadowsEnabled = ShadowPass.isEnabled(props);
const occlusionEnabled = SsaoPass.isEnabled(props);
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const v = camera.viewport;
if (props.occlusion.name === 'on') {
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
const b = this.ssaoRenderable.values.uBounds;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
Math.floor(v.y * s) / (h * s),
Math.ceil((v.x + v.width) * s) / (w * s),
Math.ceil((v.y + v.height) * s) / (h * s)
);
ValueCell.update(b, b.ref.value);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
}
if (this.nSamples !== props.occlusion.params.samples) {
needsUpdateSsao = true;
this.nSamples = props.occlusion.params.samples;
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
const multiScale = props.occlusion.params.multiScale.name === 'on';
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
}
if (props.occlusion.params.multiScale.name === 'on') {
const mp = props.occlusion.params.multiScale.params;
if (!deepEqual(this.levels, mp.levels)) {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
needsUpdateSsaoBlur = true;
this.blurKernelSize = props.occlusion.params.blurKernelSize;
const kernel = getBlurKernel(this.blurKernelSize);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
if (this.ssaoScale !== ssaoScale) {
needsUpdateSsao = true;
needsUpdateDepthHalf = true;
this.ssaoScale = ssaoScale;
const sw = Math.floor(w * this.ssaoScale);
const sh = Math.floor(h * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.floor(sw * 0.5);
const hh = Math.floor(sh * 0.5);
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.floor(sw * 0.25);
const qh = Math.floor(sh * 0.25);
this.depthQuarterTarget.setSize(qw, qh);
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
}
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
if (occlusionEnabled) {
this.ssao.update(camera, props.occlusion.params as SsaoProps);
}
if (props.shadow.name === 'on') {
ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
needsUpdateShadows = true;
}
ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
needsUpdateShadows = true;
}
if (shadowsEnabled) {
this.shadow.update(camera, light, ambientColor, props.shadow.params as ShadowProps);
}
if (props.outline.name === 'on') {
const transparentOutline = props.outline.params.includeTransparent ?? true;
const outlineScale = Math.max(1, Math.round(props.outline.params.scale * this.webgl.pixelRatio)) - 1;
const outlineThreshold = 50 * props.outline.params.threshold * this.webgl.pixelRatio;
if (outlinesEnabled) {
const outlineProps = props.outline.params as OutlineProps;
const { transparentOutline, outlineScale } = this.outline.update(camera, outlineProps, this.drawPass.depthTextureTransparent, this.drawPass.depthTextureOpaque);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection);
if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
}
if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, outlineProps.color));
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateMain = true;
@@ -811,6 +226,7 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
@@ -829,27 +245,6 @@ export class PostprocessingPass {
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
}
if (needsUpdateShadows) {
this.shadowsRenderable.update();
}
if (needsUpdateSsao) {
this.ssaoRenderable.update();
}
if (needsUpdateSsaoBlur) {
this.ssaoBlurFirstPassRenderable.update();
this.ssaoBlurSecondPassRenderable.update();
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
}
if (needsUpdateMain) {
this.renderable.update();
}
@@ -874,64 +269,28 @@ export class PostprocessingPass {
this.transparentBackground = value;
}
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
this.updateState(camera, transparentBackground, backgroundColor, props, light);
this.updateState(camera, transparentBackground, backgroundColor, props, light, ambientColor);
const { gl, state } = this.webgl;
const { state } = this.webgl;
const { x, y, width, height } = camera.viewport;
// don't render occlusion if offset is given,
// which will reuse the existing occlusion
if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
const sh = Math.ceil(height * this.ssaoScale);
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
this.ssaoFramebuffer.bind();
this.ssaoRenderable.render();
this.ssaoBlurFirstPassFramebuffer.bind();
this.ssaoBlurFirstPassRenderable.render();
this.ssaoBlurSecondPassFramebuffer.bind();
this.ssaoBlurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
this.ssao.render(camera);
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
this.outline.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
this.shadow.render();
}
if (toDrawingBuffer) {
@@ -941,21 +300,8 @@ export class PostprocessingPass {
}
this.background.update(camera, props.background);
if (this.background.isEnabled(props.background)) {
if (this.transparentBackground) {
state.clearColor(0, 0, 0, 0);
} else {
Color.toVec3Normalized(this.bgColor, backgroundColor);
state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1);
}
gl.clear(gl.COLOR_BUFFER_BIT);
state.enable(gl.BLEND);
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
this.background.render();
} else {
state.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
this.background.clear(props.background, this.transparentBackground, backgroundColor);
this.background.render(props.background);
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { isTimingMode } from '../../mol-util/debug';
import { Light } from '../../mol-gl/renderer';
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
import { PostprocessingProps } from './postprocessing';
export const ShadowParams = {
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
};
export type ShadowProps = PD.Values<typeof ShadowParams>
export class ShadowPass {
static isEnabled(props: PostprocessingProps) {
return props.shadow.name !== 'off';
}
readonly target: RenderTarget;
private readonly renderable: ShadowsRenderable;
constructor(readonly webgl: WebGLContext, width: number, height: number, depthTextureOpaque: Texture) {
this.target = webgl.createRenderTarget(width, height, false);
this.renderable = getShadowsRenderable(webgl, depthTextureOpaque);
}
setSize(width: number, height: number) {
const [w, h] = this.renderable.values.uTexSize.ref.value;
if (width !== w || height !== h) {
this.target.setSize(width, height);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
}
}
update(camera: ICamera, light: Light, ambientColor: Vec3, props: ShadowProps) {
let needsUpdateShadows = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const v = camera.viewport;
ValueCell.update(this.renderable.values.uProjection, camera.projection);
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
Vec4.set(this.renderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.renderable.values.uBounds, this.renderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance);
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance);
if (this.renderable.values.dSteps.ref.value !== props.steps) {
ValueCell.update(this.renderable.values.dSteps, props.steps);
needsUpdateShadows = true;
}
ValueCell.update(this.renderable.values.uLightDirection, light.direction);
ValueCell.update(this.renderable.values.uLightColor, light.color);
if (this.renderable.values.dLightCount.ref.value !== light.count) {
ValueCell.update(this.renderable.values.dLightCount, light.count);
needsUpdateShadows = true;
}
ValueCell.update(this.renderable.values.uAmbientColor, ambientColor);
if (needsUpdateShadows) {
this.renderable.update();
}
}
render() {
if (isTimingMode) this.webgl.timer.mark('ShadowPass.render');
this.target.bind();
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('ShadowPass.render');
}
}
const ShadowsSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
dSteps: DefineSpec('number'),
uMaxDistance: UniformSpec('f'),
uTolerance: UniformSpec('f'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
uAmbientColor: UniformSpec('v3'),
};
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
const width = depthTexture.getWidth();
const height = depthTexture.getHeight();
const values: Values<typeof ShadowsSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(width, height)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
dSteps: ValueCell.create(1),
uMaxDistance: ValueCell.create(3.0),
uTolerance: ValueCell.create(1.0),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
uAmbientColor: ValueCell.create(Vec3()),
};
const schema = { ...ShadowsSchema };
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -0,0 +1,545 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { deepEqual, ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
import { Color } from '../../mol-util/color';
import { isTimingMode } from '../../mol-util/debug';
import { PostprocessingProps } from './postprocessing';
export const SsaoParams = {
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
multiScale: PD.MappedStatic('off', {
on: PD.Group({
levels: PD.ObjectList({
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
{ radius: 2, bias: 1 },
{ radius: 5, bias: 1 },
{ radius: 8, bias: 1 },
{ radius: 11, bias: 1 },
] }),
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
}),
off: PD.Group({})
}, { cycle: true }),
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
blurDepthBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
};
export type SsaoProps = PD.Values<typeof SsaoParams>
type Levels = {
count: number
radius: number[]
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
bias: (new Array(count * 3)).fill(0),
};
props = props.slice().sort((a, b) => a.radius - b.radius);
for (let i = 0; i < count; ++i) {
const p = props[i];
radius[i] = Math.pow(2, p.radius);
bias[i] = p.bias;
}
return { count, radius, bias };
}
export class SsaoPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name !== 'off';
}
readonly target: RenderTarget;
private readonly framebuffer: Framebuffer;
private readonly blurFirstPassFramebuffer: Framebuffer;
private readonly blurSecondPassFramebuffer: Framebuffer;
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
readonly ssaoDepthTexture: Texture;
private readonly depthBlurProxyTexture: Texture;
private readonly renderable: SsaoRenderable;
private readonly blurFirstPassRenderable: SsaoBlurRenderable;
private readonly blurSecondPassRenderable: SsaoBlurRenderable;
private depthTexture: Texture;
private nSamples: number;
private blurKernelSize: number;
private texSize: [number, number];
private ssaoScale: number;
private calcSsaoScale(resolutionScale: number) {
// downscale ssao for high pixel-ratios
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
}
private levels: { radius: number, bias: number }[];
private getDepthTexture() {
return this.ssaoScale === 1 ? this.depthTexture : this.downsampledDepthTarget.texture;
}
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTexture: Texture) {
const { textureFloatLinear } = webgl.extensions;
this.depthTexture = depthTexture;
this.nSamples = 1;
this.blurKernelSize = 1;
this.ssaoScale = this.calcSsaoScale(1);
this.texSize = [width, height];
this.levels = [];
this.framebuffer = webgl.resources.framebuffer();
this.blurFirstPassFramebuffer = webgl.resources.framebuffer();
this.blurSecondPassFramebuffer = webgl.resources.framebuffer();
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
const filter = textureFloatLinear ? 'linear' : 'nearest';
this.downsampledDepthTarget = packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture);
this.depthHalfTarget = packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, this.getDepthTexture());
this.depthQuarterTarget = packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
this.depthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.depthBlurProxyTexture.define(sw, sh);
this.depthBlurProxyTexture.attachFramebuffer(this.blurFirstPassFramebuffer, 'color0');
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
this.renderable = getSsaoRenderable(webgl, this.getDepthTexture(), this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.blurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.depthBlurProxyTexture, 'vertical');
}
setSize(width: number, height: number) {
const [w, h] = this.texSize;
const ssaoScale = this.calcSsaoScale(1);
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.texSize.splice(0, 2, width, height);
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.depthBlurProxyTexture.define(sw, sh);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.depthQuarterTarget.setSize(qw, qh);
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
const depthTexture = this.getDepthTexture();
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.renderable.values.tDepth, depthTexture);
this.depthHalfRenderable.update();
this.renderable.update();
}
}
update(camera: ICamera, props: SsaoProps) {
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.texSize;
const v = camera.viewport;
ValueCell.update(this.renderable.values.uProjection, camera.projection);
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
const b = this.renderable.values.uBounds;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
Math.floor(v.y * s) / (h * s),
Math.ceil((v.x + v.width) * s) / (w * s),
Math.ceil((v.y + v.height) * s) / (h * s)
);
ValueCell.update(b, b.ref.value);
ValueCell.update(this.blurFirstPassRenderable.values.uBounds, b.ref.value);
ValueCell.update(this.blurSecondPassRenderable.values.uBounds, b.ref.value);
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uFar, camera.far);
ValueCell.update(this.blurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.blurSecondPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
if (this.blurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.blurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.blurSecondPassRenderable.values.dOrthographic, orthographic);
}
if (this.nSamples !== props.samples) {
needsUpdateSsao = true;
this.nSamples = props.samples;
ValueCell.update(this.renderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.renderable.values.dNSamples, this.nSamples);
}
const multiScale = props.multiScale.name === 'on';
if (this.renderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.renderable.values.dMultiScale, multiScale);
}
if (props.multiScale.name === 'on') {
const mp = props.multiScale.params;
if (!deepEqual(this.levels, mp.levels)) {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
ValueCell.updateIfChanged(this.renderable.values.dLevels, levels.count);
ValueCell.update(this.renderable.values.uLevelRadius, levels.radius);
ValueCell.update(this.renderable.values.uLevelBias, levels.bias);
}
ValueCell.updateIfChanged(this.renderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.renderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius));
}
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);
if (this.blurKernelSize !== props.blurKernelSize) {
needsUpdateSsaoBlur = true;
this.blurKernelSize = props.blurKernelSize;
const kernel = getBlurKernel(this.blurKernelSize);
ValueCell.update(this.blurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.blurSecondPassRenderable.values.uKernel, kernel);
ValueCell.update(this.blurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.blurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
const ssaoScale = this.calcSsaoScale(props.resolutionScale);
if (this.ssaoScale !== ssaoScale) {
needsUpdateSsao = true;
needsUpdateDepthHalf = true;
this.ssaoScale = ssaoScale;
const sw = Math.floor(w * this.ssaoScale);
const sh = Math.floor(h * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.depthBlurProxyTexture.define(sw, sh);
const hw = Math.floor(sw * 0.5);
const hh = Math.floor(sh * 0.5);
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.floor(sw * 0.25);
const qh = Math.floor(sh * 0.25);
this.depthQuarterTarget.setSize(qw, qh);
const depthTexture = this.getDepthTexture();
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.renderable.values.tDepth, depthTexture);
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
}
if (needsUpdateSsao) {
this.renderable.update();
}
if (needsUpdateSsaoBlur) {
this.blurFirstPassRenderable.update();
this.blurSecondPassRenderable.update();
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
}
}
render(camera: ICamera) {
if (isTimingMode) this.webgl.timer.mark('SsaoPass.render');
const { state } = this.webgl;
const { x, y, width, height } = camera.viewport;
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
const sh = Math.ceil(height * this.ssaoScale);
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SsaoPass.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SsaoPass.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.half');
if (isTimingMode) this.webgl.timer.mark('SsaoPass.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.quarter');
this.framebuffer.bind();
this.renderable.render();
this.blurFirstPassFramebuffer.bind();
this.blurFirstPassRenderable.render();
this.blurSecondPassFramebuffer.bind();
this.blurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.render');
}
}
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uTexSize: UniformSpec('v2'),
uRadius: UniformSpec('f'),
uBias: UniformSpec('f'),
dMultiScale: DefineSpec('boolean'),
dLevels: DefineSpec('number'),
uLevelRadius: UniformSpec('f[]'),
uLevelBias: UniformSpec('f[]'),
uNearThreshold: UniformSpec('f'),
uFarThreshold: UniformSpec('f'),
};
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
const values: Values<typeof SsaoSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
tDepthHalf: ValueCell.create(depthHalfTexture),
tDepthQuarter: ValueCell.create(depthQuarterTexture),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
uRadius: ValueCell.create(Math.pow(2, 5)),
uBias: ValueCell.create(0.8),
dMultiScale: ValueCell.create(false),
dLevels: ValueCell.create(3),
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
uNearThreshold: ValueCell.create(10.0),
uFarThreshold: ValueCell.create(1500.0),
};
const schema = { ...SsaoSchema };
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoBlurSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uKernel: UniformSpec('f[]'),
dOcclusionKernelSize: DefineSpec('number'),
uBlurDepthBias: UniformSpec('f'),
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
};
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
const values: Values<typeof SsaoBlurSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
uKernel: ValueCell.create(getBlurKernel(15)),
dOcclusionKernelSize: ValueCell.create(15),
uBlurDepthBias: ValueCell.create(0.5),
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
};
const schema = { ...SsaoBlurSchema };
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
function getBlurKernel(kernelSize: number): number[] {
const sigma = kernelSize / 3.0;
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
const kernel = [];
for (let x = 0; x < halfKernelSize; x++) {
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
}
return kernel;
}
const RandomHemisphereVector: Vec3[] = [];
for (let i = 0; i < 256; i++) {
const v = Vec3();
v[0] = Math.random() * 2.0 - 1.0;
v[1] = Math.random() * 2.0 - 1.0;
v[2] = Math.random();
Vec3.normalize(v, v);
Vec3.scale(v, v, Math.random());
RandomHemisphereVector.push(v);
}
function getSamples(nSamples: number): number[] {
const samples = [];
for (let i = 0; i < nSamples; i++) {
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
scale = 0.1 + scale * (1.0 - 0.1);
samples.push(RandomHemisphereVector[i][0] * scale);
samples.push(RandomHemisphereVector[i][1] * scale);
samples.push(RandomHemisphereVector[i][2] * scale);
}
return samples;
}

View File

@@ -0,0 +1,508 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
import { Texture } from '../../mol-gl/webgl/texture';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ValueCell } from '../../mol-util';
import { isTimingMode } from '../../mol-util/debug';
import { Renderer } from '../../mol-gl/renderer';
import { Camera, ICamera } from '../camera';
import { Scene } from '../../mol-gl/scene';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { ShaderCode } from '../../mol-gl/shader-code';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
import { trace_frag } from '../../mol-gl/shader/illumination/trace.frag';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color/color';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
import { Helper } from '../helper/helper';
import { accumulate_frag } from '../../mol-gl/shader/illumination/accumulate.frag';
import { now } from '../../mol-util/now';
import { clamp } from '../../mol-math/interpolate';
type RenderContext = {
renderer: Renderer;
camera: Camera;
scene: Scene;
helper: Helper;
}
export const TracingParams = {
rendersPerFrame: PD.Interval([1, 16], { min: 1, max: 64, step: 1 }, { description: 'Number of rays per pixel each frame. May be adjusted to reach targetFps but will stay within given interval.' }),
targetFps: PD.Numeric(30, { min: 0, max: 120, step: 0.1 }, { description: 'Target FPS per frame. If observed FPS is lower or higher, some parameters may get adjusted.' }),
steps: PD.Numeric(32, { min: 1, max: 1024, step: 1 }),
firstStepSize: PD.Numeric(0.01, { min: 0.001, max: 1, step: 0.001 }),
refineSteps: PD.Numeric(4, { min: 0, max: 8, step: 1 }, { description: 'Number of refine steps per ray hit. May be lower to reach targetFps.' }),
rayDistance: PD.Numeric(256, { min: 1, max: 8192, step: 1 }, { description: 'Maximum distance a ray can travel (in world units).' }),
thicknessMode: PD.Select('auto', PD.arrayToOptions(['auto', 'fixed'] as const)),
minThickness: PD.Numeric(0.5, { min: 0.1, max: 16, step: 0.1 }, { hideIf: p => p.thicknessMode === 'fixed' }),
thicknessFactor: PD.Numeric(1, { min: 0.1, max: 2, step: 0.05 }, { hideIf: p => p.thicknessMode === 'fixed' }),
thickness: PD.Numeric(4, { min: 0.1, max: 512, step: 0.1 }, { hideIf: p => p.thicknessMode === 'auto' }),
bounces: PD.Numeric(4, { min: 1, max: 32, step: 1 }, { description: 'Number of bounces for each ray.' }),
glow: PD.Boolean(true, { description: 'Bounced rays always get the full light. This produces a slight glowing effect.' }),
shadowEnable: PD.Boolean(false),
shadowSoftness: PD.Numeric(0.1, { min: 0.01, max: 1.0, step: 0.01 }),
shadowThickness: PD.Numeric(0.5, { min: 0.1, max: 32, step: 0.1 }),
};
export type TracingProps = PD.Values<typeof TracingParams>
export class TracingPass {
private readonly framebuffer: Framebuffer;
readonly colorTextureOpaque: Texture;
readonly normalTextureOpaque: Texture;
readonly shadedTextureOpaque: Texture;
readonly depthTextureOpaque: Texture;
private readonly thicknessTarget: RenderTarget;
private readonly holdTarget: RenderTarget;
readonly accumulateTarget: RenderTarget;
readonly composeTarget: RenderTarget;
private readonly traceRenderable: TraceRenderable;
private readonly accumulateRenderable: AccumulateRenderable;
constructor(private readonly webgl: WebGLContext, width: number, height: number) {
const { extensions: { drawBuffers, colorBufferHalfFloat, textureHalfFloat }, resources, isWebGL2 } = webgl;
if (isWebGL2) {
this.shadedTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.shadedTextureOpaque.define(width, height);
this.normalTextureOpaque = colorBufferHalfFloat && textureHalfFloat
? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.normalTextureOpaque.define(width, height);
this.colorTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.colorTextureOpaque.define(width, height);
} else {
// webgl1 requires consistent bit plane counts
this.shadedTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.shadedTextureOpaque.define(width, height);
this.normalTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.normalTextureOpaque.define(width, height);
this.colorTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.colorTextureOpaque.define(width, height);
}
this.depthTextureOpaque = resources.texture('image-depth', 'depth', isWebGL2 ? 'float' : 'ushort', 'nearest');
this.depthTextureOpaque.define(width, height);
this.framebuffer = resources.framebuffer();
this.framebuffer.bind();
drawBuffers!.drawBuffers([
drawBuffers!.COLOR_ATTACHMENT0,
drawBuffers!.COLOR_ATTACHMENT1,
drawBuffers!.COLOR_ATTACHMENT2,
]);
this.shadedTextureOpaque.attachFramebuffer(this.framebuffer, 'color0');
this.normalTextureOpaque.attachFramebuffer(this.framebuffer, 'color1');
this.colorTextureOpaque.attachFramebuffer(this.framebuffer, 'color2');
this.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
this.thicknessTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'nearest');
this.holdTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.accumulateTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.composeTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.traceRenderable = getTraceRenderable(webgl, this.colorTextureOpaque, this.normalTextureOpaque, this.shadedTextureOpaque, this.thicknessTarget.texture, this.accumulateTarget.texture, this.depthTextureOpaque);
this.accumulateRenderable = getAccumulateRenderable(webgl, this.holdTarget.texture);
}
private renderInput(renderer: Renderer, camera: ICamera, scene: Scene, props: TracingProps) {
if (isTimingMode) this.webgl.timer.mark('TracePass.renderInput');
const { gl, state } = this.webgl;
this.framebuffer.bind();
this.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
renderer.clear(true);
renderer.renderTracing(scene.primitives, camera, null);
//
if (props.thicknessMode === 'auto') {
this.thicknessTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
renderer.renderDepthOpaqueBack(scene.primitives, camera, null);
}
if (isTimingMode) this.webgl.timer.markEnd('TracePass.renderInput');
}
setSize(width: number, height: number) {
const w = this.composeTarget.getWidth();
const h = this.composeTarget.getHeight();
if (width !== w || height !== h) {
this.thicknessTarget.setSize(width, height);
this.holdTarget.setSize(width, height);
this.accumulateTarget.setSize(width, height);
this.composeTarget.setSize(width, height);
this.colorTextureOpaque.define(width, height);
this.normalTextureOpaque.define(width, height);
this.shadedTextureOpaque.define(width, height);
this.depthTextureOpaque.define(width, height);
ValueCell.update(this.traceRenderable.values.uTexSize, Vec2.set(this.traceRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.accumulateRenderable.values.uTexSize, Vec2.set(this.accumulateRenderable.values.uTexSize.ref.value, width, height));
}
}
private clearAdjustedProps = true;
reset(clearAdjustedProps = false) {
const { gl, state } = this.webgl;
this.accumulateTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this.composeTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
if (clearAdjustedProps) {
this.prevTime = 0;
this.currTime = 0;
this.clearAdjustedProps = true;
}
}
private prevTime = 0;
private currTime = 0;
private rendersPerFrame = 1;
private refineSteps = 1;
private steps = 16;
private increaseAdjustedProps(props: TracingProps) {
this.steps += 1;
if (this.steps > props.steps) {
this.refineSteps += 1;
}
if (this.refineSteps > props.refineSteps) {
this.rendersPerFrame += 1;
}
}
private decreaseAdjustedProps(props: TracingProps) {
const minRefineSteps = Math.min(1, props.refineSteps);
this.rendersPerFrame -= 1;
if (this.rendersPerFrame < 1) {
this.refineSteps -= 1;
}
if (this.refineSteps < minRefineSteps) {
this.steps -= 1;
}
}
private getAdjustedProps(props: TracingProps, iteration: number) {
this.currTime = now();
const minRefineSteps = Math.min(1, props.refineSteps);
const minSteps = Math.round(props.steps / 2);
if (this.clearAdjustedProps) {
this.rendersPerFrame = props.rendersPerFrame[0];
this.refineSteps = minRefineSteps;
this.steps = minSteps;
this.clearAdjustedProps = false;
}
if (iteration > 0) {
const targetTimeMs = 1000 / props.targetFps;
const deltaTime = this.currTime - this.prevTime;
let f = Math.round(deltaTime / targetTimeMs);
if (f >= 2) {
while (f > 0) {
this.decreaseAdjustedProps(props);
f -= 1;
}
} else if (deltaTime < targetTimeMs) {
this.increaseAdjustedProps(props);
} else if (deltaTime > targetTimeMs + 0.5) {
this.decreaseAdjustedProps(props);
}
}
this.prevTime = this.currTime;
this.rendersPerFrame = clamp(this.rendersPerFrame, props.rendersPerFrame[0], props.rendersPerFrame[1]);
this.refineSteps = clamp(this.refineSteps, minRefineSteps, props.refineSteps);
this.steps = clamp(this.steps, minSteps, props.steps);
return {
rendersPerFrame: iteration === 0 ? Math.ceil(this.rendersPerFrame / 2) : this.rendersPerFrame,
refineSteps: iteration === 0 ? minRefineSteps : this.refineSteps,
steps: iteration === 0 ? minSteps : this.steps,
};
}
render(ctx: RenderContext, transparentBackground: boolean, props: TracingProps, iteration: number, forceRenderInput: boolean) {
const { rendersPerFrame, refineSteps, steps } = this.getAdjustedProps(props, iteration);
if (isTimingMode) {
this.webgl.timer.mark('TracePass.render', {
note: `${rendersPerFrame} rendersPerFrame, ${refineSteps} refineSteps, ${steps} steps`
});
}
const { renderer, camera, scene } = ctx;
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
if (iteration === 0 || forceRenderInput) {
// render color & depth
renderer.setTransparentBackground(transparentBackground);
renderer.setDrawingBufferSize(this.composeTarget.getWidth(), this.composeTarget.getHeight());
renderer.setPixelRatio(this.webgl.pixelRatio);
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
this.renderInput(renderer, camera, scene, props);
}
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.disable(gl.CULL_FACE);
state.depthMask(false);
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const [w, h] = this.traceRenderable.values.uTexSize.ref.value;
const v = camera.viewport;
const ambientColor = Vec3();
Vec3.scale(ambientColor, Color.toArrayNormalized(renderer.props.ambientColor, ambientColor, 0), renderer.props.ambientIntensity);
const lightStrength = Vec3.clone(ambientColor);
for (let i = 0, il = renderer.light.count; i < il; ++i) {
const light = Vec3.fromArray(Vec3(), renderer.light.color, i * 3);
Vec3.add(lightStrength, lightStrength, light);
}
// trace
this.holdTarget.bind();
let needsUpdateTrace = false;
ValueCell.update(this.traceRenderable.values.uFrameNo, iteration);
if (this.traceRenderable.values.dRendersPerFrame.ref.value !== rendersPerFrame) {
ValueCell.update(this.traceRenderable.values.dRendersPerFrame, rendersPerFrame);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uProjection, camera.projection);
ValueCell.update(this.traceRenderable.values.uInvProjection, invProjection);
Vec4.set(this.traceRenderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.traceRenderable.values.uBounds, this.traceRenderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.traceRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.traceRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.traceRenderable.values.uFogFar, camera.fogFar);
ValueCell.updateIfChanged(this.traceRenderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.traceRenderable.values.uFogColor, Color.toVec3Normalized(this.traceRenderable.values.uFogColor.ref.value, renderer.props.backgroundColor));
if (this.traceRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.traceRenderable.values.dOrthographic, orthographic);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uLightDirection, renderer.light.direction);
ValueCell.update(this.traceRenderable.values.uLightColor, renderer.light.color);
if (this.traceRenderable.values.dLightCount.ref.value !== renderer.light.count) {
ValueCell.update(this.traceRenderable.values.dLightCount, renderer.light.count);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uAmbientColor, ambientColor);
ValueCell.update(this.traceRenderable.values.uLightStrength, lightStrength);
if (this.traceRenderable.values.dGlow.ref.value !== props.glow) {
ValueCell.update(this.traceRenderable.values.dGlow, props.glow);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dBounces.ref.value !== props.bounces) {
ValueCell.update(this.traceRenderable.values.dBounces, props.bounces);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dSteps.ref.value !== steps) {
ValueCell.update(this.traceRenderable.values.dSteps, steps);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dFirstStepSize.ref.value !== props.firstStepSize) {
ValueCell.update(this.traceRenderable.values.dFirstStepSize, props.firstStepSize);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dRefineSteps.ref.value !== refineSteps) {
ValueCell.update(this.traceRenderable.values.dRefineSteps, refineSteps);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uRayDistance, props.rayDistance);
if (this.traceRenderable.values.dThicknessMode.ref.value !== props.thicknessMode) {
ValueCell.update(this.traceRenderable.values.dThicknessMode, props.thicknessMode);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uMinThickness, props.minThickness);
ValueCell.updateIfChanged(this.traceRenderable.values.uThicknessFactor, props.thicknessFactor);
ValueCell.updateIfChanged(this.traceRenderable.values.uThickness, props.thickness);
if (this.traceRenderable.values.dShadowEnable.ref.value !== props.shadowEnable) {
ValueCell.update(this.traceRenderable.values.dShadowEnable, props.shadowEnable);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowSoftness, props.shadowSoftness);
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowThickness, props.shadowThickness);
if (needsUpdateTrace) this.traceRenderable.update();
if (isTimingMode) this.webgl.timer.mark('TracePass.renderTrace');
this.traceRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('TracePass.renderTrace');
// accumulate
this.accumulateTarget.bind();
this.accumulateRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('TracePass.render');
}
}
//
const TraceSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tThickness: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tAccumulate: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uFogNear: UniformSpec('f'),
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
uAmbientColor: UniformSpec('v3'),
uLightStrength: UniformSpec('v3'),
uFrameNo: UniformSpec('i'),
dRendersPerFrame: DefineSpec('number'),
dGlow: DefineSpec('boolean'),
dBounces: DefineSpec('number'),
dSteps: DefineSpec('number'),
dFirstStepSize: DefineSpec('number'),
dRefineSteps: DefineSpec('number'),
uRayDistance: UniformSpec('f'),
dThicknessMode: DefineSpec('string'),
uMinThickness: UniformSpec('f'),
uThicknessFactor: UniformSpec('f'),
uThickness: UniformSpec('f'),
dShadowEnable: DefineSpec('boolean'),
uShadowSoftness: UniformSpec('f'),
uShadowThickness: UniformSpec('f'),
};
const TraceShaderCode = ShaderCode('trace', quad_vert, trace_frag);
type TraceRenderable = ComputeRenderable<Values<typeof TraceSchema>>
function getTraceRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, thicknessTexture: Texture, accumulateTexture: Texture, depthTexture: Texture): TraceRenderable {
const values: Values<typeof TraceSchema> = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
tNormal: ValueCell.create(normalTexture),
tShaded: ValueCell.create(shadedTexture),
tThickness: ValueCell.create(thicknessTexture),
tAccumulate: ValueCell.create(accumulateTexture),
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uFogNear: ValueCell.create(10000),
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
uAmbientColor: ValueCell.create(Vec3()),
uLightStrength: ValueCell.create(Vec3.create(1, 1, 1)),
uFrameNo: ValueCell.create(0),
dRendersPerFrame: ValueCell.create(1),
dGlow: ValueCell.create(true),
dBounces: ValueCell.create(4),
dSteps: ValueCell.create(32),
dFirstStepSize: ValueCell.create(0.01),
dRefineSteps: ValueCell.create(4),
uRayDistance: ValueCell.create(256),
dThicknessMode: ValueCell.create('auto'),
uMinThickness: ValueCell.create(0.5),
uThicknessFactor: ValueCell.create(1),
uThickness: ValueCell.create(4),
dShadowEnable: ValueCell.create(false),
uShadowSoftness: ValueCell.create(0.1),
uShadowThickness: ValueCell.create(0.1),
};
const schema = { ...TraceSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', TraceShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
//
const AccumulateSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uWeight: UniformSpec('f'),
};
const AccumulateShaderCode = ShaderCode('accumulate', quad_vert, accumulate_frag);
type AccumulateRenderable = ComputeRenderable<Values<typeof AccumulateSchema>>
function getAccumulateRenderable(ctx: WebGLContext, colorTexture: Texture): AccumulateRenderable {
const values: Values<typeof AccumulateSchema> = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
uWeight: ValueCell.create(1.0),
};
const schema = { ...AccumulateSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', AccumulateShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -87,6 +87,7 @@ export namespace BaseGeometry {
material: Material.getParam(),
clip: PD.Group(Clip.Params),
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
@@ -118,6 +119,7 @@ export namespace BaseGeometry {
uRoughness: ValueCell.create(props.material.roughness),
uBumpiness: ValueCell.create(props.material.bumpiness),
uEmissive: ValueCell.create(props.emissive),
uDensity: ValueCell.create(props.density),
dLightCount: ValueCell.create(1),
dColorMarker: ValueCell.create(true),
@@ -140,6 +142,7 @@ export namespace BaseGeometry {
ValueCell.updateIfChanged(values.uRoughness, props.material.roughness);
ValueCell.updateIfChanged(values.uBumpiness, props.material.bumpiness);
ValueCell.updateIfChanged(values.uEmissive, props.emissive);
ValueCell.updateIfChanged(values.uDensity, props.density);
const clip = Clip.getClip(props.clip);
ValueCell.updateIfChanged(values.dClipObjectCount, clip.objects.count);

View File

@@ -168,6 +168,7 @@ export namespace Cylinders {
sizeAspectRatio: PD.Numeric(1, { min: 0, max: 3, step: 0.01 }),
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
@@ -259,6 +260,7 @@ export namespace Cylinders {
uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
uDoubleSided: ValueCell.create(props.doubleSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
dSolidInterior: ValueCell.create(props.solidInterior),
@@ -279,6 +281,7 @@ export namespace Cylinders {
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor * props.sizeAspectRatio);
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);

View File

@@ -148,6 +148,7 @@ export namespace DirectVolume {
export const Params = {
...BaseGeometry.Params,
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
controlPoints: PD.LineGraph([
Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
@@ -272,6 +273,7 @@ export namespace DirectVolume {
dAxisOrder: ValueCell.create(directVolume.axisOrder.ref.value.join('')),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
};
}
@@ -285,6 +287,7 @@ export namespace DirectVolume {
function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
BaseGeometry.updateValues(values, props);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { Vec3 } from '../../../../mol-math/linear-algebra';
@@ -10,6 +11,7 @@ import { cantorPairing, ChunkedArray } from '../../../../mol-data/util';
import { MeshBuilder } from '../mesh-builder';
const normalVector = Vec3();
const capNormalSmoothingVector = Vec3();
const surfacePoint = Vec3();
const controlPoint = Vec3();
const u = Vec3();
@@ -32,6 +34,7 @@ const v3fromArray = Vec3.fromArray;
const v3normalize = Vec3.normalize;
const v3scaleAndAdd = Vec3.scaleAndAdd;
const v3cross = Vec3.cross;
const v3slerp = Vec3.slerp;
const v3dot = Vec3.dot;
const v3unitX = Vec3.unitX;
const caAdd3 = ChunkedArray.add3;
@@ -53,7 +56,7 @@ function getCosSin(radialSegments: number, shift: boolean) {
return CosSinCache.get(hash)!;
}
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded') {
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded', roundCap = false) {
const { currentGroup, vertices, normals, indices, groups } = state;
let vertexCount = vertices.elementCount;
@@ -63,14 +66,23 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
const q1 = Math.round(radialSegments / 4);
const q3 = q1 * 3;
const roundCapFlag = roundCap && linearSegments && !(startCap && endCap) && (startCap || endCap); // disabled if both caps are active
for (let i = 0; i <= linearSegments; ++i) {
const i3 = i * 3;
v3fromArray(u, normalVectors, i3);
v3fromArray(v, binormalVectors, i3);
v3fromArray(controlPoint, controlPoints, i3);
const width = widthValues[i];
const height = heightValues[i];
let width = widthValues[i];
let height = heightValues[i];
let capSmoothingFactor: number;
if (roundCapFlag) {
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((startCap ? linearSegments - i : i) / (linearSegments), 2)));
width *= capSmoothingFactor;
height *= capSmoothingFactor;
v3cross(capNormalSmoothingVector, startCap ? v : u, startCap ? u : v);
v3normalize(capNormalSmoothingVector, capNormalSmoothingVector);
}
const rounded = crossSection === 'rounded' && height > width;
for (let j = 0; j < radialSegments; ++j) {
@@ -94,6 +106,9 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
v3normalize(normalVector, normalVector);
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
if (roundCapFlag) {
v3slerp(normalVector, capNormalSmoothingVector, normalVector, capSmoothingFactor!);
}
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
}
}
@@ -144,8 +159,8 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
const width = widthValues[0];
let height = heightValues[0];
const width = roundCapFlag ? 0 : widthValues[0];
let height = roundCapFlag ? 0 : heightValues[0];
const rounded = crossSection === 'rounded' && height > width;
if (rounded) height -= width;
@@ -181,8 +196,8 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
const width = widthValues[linearSegments];
let height = heightValues[linearSegments];
const width = roundCapFlag ? 0 : widthValues[linearSegments];
let height = roundCapFlag ? 0 : heightValues[linearSegments];
const rounded = crossSection === 'rounded' && height > width;
if (rounded) height -= width;

View File

@@ -628,6 +628,7 @@ export namespace Mesh {
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
@@ -713,6 +714,7 @@ export namespace Mesh {
dFlatShaded: ValueCell.create(props.flatShaded),
dFlipSided: ValueCell.create(props.flipSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
@@ -734,6 +736,7 @@ export namespace Mesh {
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);

View File

@@ -247,6 +247,7 @@ export namespace Spheres {
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
@@ -346,6 +347,7 @@ export namespace Spheres {
uSizeFactor: spheres.shaderData.sizeFactor,
uDoubleSided: ValueCell.create(props.doubleSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
dSolidInterior: ValueCell.create(props.solidInterior),
@@ -372,6 +374,7 @@ export namespace Spheres {
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);

View File

@@ -121,6 +121,7 @@ export namespace TextureMesh {
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
@@ -226,6 +227,7 @@ export namespace TextureMesh {
dFlatShaded: ValueCell.create(props.flatShaded),
dFlipSided: ValueCell.create(props.flipSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
@@ -247,6 +249,7 @@ export namespace TextureMesh {
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);

View File

@@ -115,6 +115,30 @@ export function LocationIterator(groupCount: number, instanceCount: number, stri
};
}
export const EmptyLocationIterator: LocationIterator = {
get hasNext() { return false; },
get isNextNewInstance() { return false; },
groupCount: 0,
instanceCount: 0,
count: 0,
stride: 0,
nonInstanceable: false,
hasLocation2: false,
move() {
return {
location: NullLocation as Location,
location2: NullLocation as Location,
index: 0,
groupIndex: 0,
instanceIndex: 0,
isSecondary: false
};
},
reset() {},
skipInstance() {},
voidInstances() {}
};
//
/** A position Location */

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/

View File

@@ -26,6 +26,7 @@ export const CylindersSchema = {
padding: ValueSpec('number'),
uDoubleSided: UniformSpec('b', 'material'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
dSolidInterior: DefineSpec('boolean'),

View File

@@ -40,6 +40,7 @@ export const DirectVolumeSchema = {
dAxisOrder: DefineSpec('string', ['012', '021', '102', '120', '201', '210']),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
};
export type DirectVolumeSchema = typeof DirectVolumeSchema

View File

@@ -22,6 +22,7 @@ export const MeshSchema = {
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),

View File

@@ -169,6 +169,7 @@ export const GlobalUniformSchema = {
uMarkerAverage: UniformSpec('f'),
uXrayEdgeFalloff: UniformSpec('f'),
uCelSteps: UniformSpec('f'),
uExposure: UniformSpec('f'),
uRenderMask: UniformSpec('i'),
@@ -338,6 +339,9 @@ export const BaseSchema = {
uBumpiness: UniformSpec('f', 'material'),
uEmissive: UniformSpec('f', 'material'),
/** density value to estimate object thickness */
uDensity: UniformSpec('f', 'material'),
uVertexCount: UniformSpec('i'),
uInstanceCount: UniformSpec('i'),
uGroupCount: UniformSpec('i'),

View File

@@ -21,6 +21,7 @@ export const SpheresSchema = {
padding: ValueSpec('number'),
uDoubleSided: UniformSpec('b', 'material'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
dSolidInterior: DefineSpec('boolean'),

View File

@@ -22,6 +22,7 @@ export const TextureMeshSchema = {
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),

View File

@@ -58,6 +58,7 @@ interface Renderer {
readonly stats: RendererStats
readonly props: Readonly<RendererProps>
readonly light: Readonly<Light>
readonly ambientColor: Vec3
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => void
clearDepth: (packed?: boolean) => void
@@ -66,10 +67,12 @@ interface Renderer {
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', depthTexture: Texture | null, pickType: PickType) => void
renderDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthOpaqueBack: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderEmissive: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderTracing: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderBlended: (group: Scene, camera: ICamera) => void
renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
@@ -109,6 +112,7 @@ export const RendererParams = {
markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]),
xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
celSteps: PD.Numeric(5, { min: 2, max: 16, step: 1 }),
exposure: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.01 }),
light: PD.ObjectList({
@@ -258,6 +262,7 @@ namespace Renderer {
uMarkerAverage: ValueCell.create(0),
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
uCelSteps: ValueCell.create(p.celSteps),
uExposure: ValueCell.create(p.exposure),
};
const globalUniformList = Object.entries(globalUniforms);
@@ -502,6 +507,26 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthOpaque');
};
const renderDepthOpaqueBack = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepthOpaqueBack');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
state.depthFunc(gl.GREATER);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (checkOpaque(r)) {
renderObject(r, 'depth', Flag.BlendedBack);
}
}
state.depthFunc(gl.LESS);
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthOpaqueBack');
};
const renderDepthTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepthTransparent');
state.disable(gl.BLEND);
@@ -579,6 +604,24 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderEmissive');
};
const renderTracing = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderTracing');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (checkOpaque(r)) {
renderObject(r, 'tracing', Flag.None);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderTracing');
};
const renderBlended = (scene: Scene, camera: ICamera) => {
if (scene.hasOpaque) {
renderBlendedOpaque(scene, camera, null);
@@ -762,14 +805,14 @@ namespace Renderer {
},
clearDepth: (packed = false) => {
state.enable(gl.SCISSOR_TEST);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
if (packed) {
state.colorMask(true, true, true, true);
state.clearColor(1, 1, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
} else {
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
gl.clear(gl.DEPTH_BUFFER_BIT);
}
},
@@ -778,10 +821,12 @@ namespace Renderer {
renderPick,
renderDepth,
renderDepthOpaque,
renderDepthOpaqueBack,
renderDepthTransparent,
renderMarkingDepth,
renderMarkingMask,
renderEmissive,
renderTracing,
renderBlended,
renderBlendedOpaque,
renderBlendedTransparent,
@@ -853,6 +898,12 @@ namespace Renderer {
p.xrayEdgeFalloff = props.xrayEdgeFalloff;
ValueCell.update(globalUniforms.uXrayEdgeFalloff, p.xrayEdgeFalloff);
}
if (props.celSteps !== undefined && props.celSteps !== p.celSteps) {
p.celSteps = props.celSteps;
ValueCell.update(globalUniforms.uCelSteps, p.celSteps);
}
if (props.exposure !== undefined && props.exposure !== p.exposure) {
p.exposure = props.exposure;
ValueCell.update(globalUniforms.uExposure, p.exposure);
@@ -919,6 +970,9 @@ namespace Renderer {
get light(): Light {
return light;
},
get ambientColor(): Vec3 {
return globalUniforms.uAmbientColor.ref.value;
},
dispose: () => {
// TODO
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ValueCell } from '../mol-util';
@@ -163,18 +164,21 @@ export function ShaderCode(name: string, vert: string, frag: string, extensions:
// Note: `drawBuffers` need to be 'optional' for wboit
function ignoreDefine(name: string, variant: string, defines: ShaderDefines): boolean {
if (variant.startsWith('color')) {
if (variant.startsWith('color') || variant === 'tracing') {
if (name === 'dLightCount') {
return !!defines.dIgnoreLight?.ref.value;
}
} else {
const ignore = [
'dColorType', 'dUsePalette',
'dLightCount', 'dXrayShaded',
'dOverpaintType', 'dOverpaint',
'dSubstanceType', 'dSubstance',
'dColorMarker',
'dColorMarker', 'dCelShaded',
'dLightCount',
];
if (variant !== 'depth') {
ignore.push('dXrayShaded');
}
if (variant !== 'emissive') {
ignore.push('dEmissiveType', 'dEmissive');
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*
* adapted from three.js (https://github.com/mrdoob/three.js/)
* which under the MIT License, Copyright © 2010-2021 three.js authors
@@ -30,7 +31,16 @@ export const apply_light_color = `
vec4 color = material;
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#if defined(dCelShaded)
// clamp to avoid artifacts
metalness = clamp(metalness, 0.0, 0.99);
roughness = clamp(roughness, 0.05, 1.0);
#endif
GeometricContext geometry;
geometry.position = -vViewPosition;
geometry.normal = normal;
geometry.viewDir = normalize(vViewPosition);
PhysicalMaterial physicalMaterial;
physicalMaterial.diffuseColor = color.rgb * (1.0 - metalness);
@@ -44,30 +54,53 @@ export const apply_light_color = `
physicalMaterial.specularColor = mix(vec3(0.04), color.rgb, metalness);
physicalMaterial.specularF90 = 1.0;
GeometricContext geometry;
geometry.position = -vViewPosition;
geometry.normal = normal;
geometry.viewDir = normalize(vViewPosition);
IncidentLight directLight;
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
}
#pragma unroll_loop_end
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
vec3 outgoingLight = vec3(0.0);
// indirect specular only metals
vec3 radiance = uAmbientColor * metalness;
vec3 iblIrradiance = uAmbientColor * metalness;
vec3 clearcoatRadiance = vec3(0.0);
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
#if defined(dCelShaded)
float celDiffuse;
float celSpecular;
float celIntensity;
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
celDiffuse = RECIPROCAL_PI * max(dot(geometry.normal, directLight.direction), 0.0) * (1.0 - metalness);
celSpecular = luminance(saturate(dot(geometry.normal, directLight.direction)) * BRDF_GGX(directLight.direction, geometry.viewDir, geometry.normal, physicalMaterial.specularColor, physicalMaterial.specularF90, roughness));
celIntensity = celDiffuse + celSpecular;
celIntensity = ceil(celIntensity * uCelSteps) / uCelSteps;
outgoingLight += color.rgb * directLight.color * celIntensity;
}
#pragma unroll_loop_end
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
#else
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
}
#pragma unroll_loop_end
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
// indirect specular only metals
vec3 radiance = uAmbientColor * metalness;
vec3 iblIrradiance = uAmbientColor * metalness;
vec3 clearcoatRadiance = vec3(0.0);
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
#endif
outgoingLight = clamp(outgoingLight, 0.01, 0.99); // prevents black artifacts on specular highlight with transparent background
#if defined(dRenderVariant_color)

View File

@@ -1,5 +1,5 @@
export const assign_color_varying = `
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dColorType_attribute)
vColor.rgb = aColor;
#elif defined(dColorType_instance)

View File

@@ -6,7 +6,7 @@ export const assign_material_color = `
}
#endif
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dUsePalette)
vec4 material = vec4(texture2D(tPalette, vec2(vPaletteV, 0.5)).rgb, uAlpha);
#elif defined(dColorType_uniform)
@@ -96,7 +96,7 @@ export const assign_material_color = `
#endif
// apply per-group transparency
#if defined(dTransparency) && (defined(dRenderVariant_pick) || defined(dRenderVariant_color) || defined(dRenderVariant_emissive))
#if defined(dTransparency) && (defined(dRenderVariant_pick) || defined(dRenderVariant_color) || defined(dRenderVariant_emissive) || defined(dRenderVariant_tracing))
float ta = 1.0 - vTransparency;
if (vTransparency < 0.09) ta = 1.0; // hard cutoff looks better
@@ -106,7 +106,7 @@ export const assign_material_color = `
#elif defined(dRenderVariant_emissive)
if (ta < 1.0)
discard; // emissive not supported with transparency
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
material.a *= ta;
#endif
#endif

View File

@@ -1,5 +1,5 @@
export const check_transparency = `
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dTransparentBackfaces_off)
if (interior && material.a < 1.0) discard;
#elif defined(dTransparentBackfaces_opaque)

View File

@@ -8,7 +8,10 @@ uniform float uBumpiness;
#endif
uniform float uEmissive;
#if defined(dRenderVariant_color)
// Density value to estimate object thickness
uniform float uDensity;
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dColorType_uniform)
uniform vec3 uColor;
#elif defined(dColorType_varying)

View File

@@ -3,7 +3,7 @@ uniform float uMetalness;
uniform float uRoughness;
uniform float uBumpiness;
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dColorType_uniform)
uniform vec3 uColor;
#elif defined(dColorType_attribute)

View File

@@ -76,6 +76,7 @@ uniform vec3 uInteriorColor;
bool interior;
uniform float uXrayEdgeFalloff;
uniform float uCelSteps;
uniform float uExposure;
uniform mat4 uProjection;

View File

@@ -17,7 +17,7 @@ export const common = `
#define dColorType_varying
#endif
#if (defined(dRenderVariant_color) && defined(dColorMarker)) || defined(dRenderVariant_marking)
#if ((defined(dRenderVariant_color) || defined(dRenderVariant_tracing)) && defined(dColorMarker)) || defined(dRenderVariant_marking)
#define dNeedsMarker
#endif
@@ -34,6 +34,9 @@ export const common = `
#define PI 3.14159265
#define RECIPROCAL_PI 0.31830988618
#define EPSILON 1e-6
#define ONE_MINUS_EPSILON 1.0 - EPSILON
#define TWO_PI 6.2831853
#define HALF_PI 1.570796325
#define saturate(a) clamp(a, 0.0, 1.0)
@@ -103,6 +106,12 @@ vec4 linearTosRGB(const in vec4 c) {
return vec4(mix(pow(c.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), c.rgb * 12.92, vec3(lessThanEqual(c.rgb, vec3(0.0031308)))), c.a);
}
float luminance(vec3 c) {
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
return dot(c, W);
}
float linearizeDepth(const in float depth, const in float near, const in float far) {
return (2.0 * near) / (far + near - depth * (far - near));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,7 +12,7 @@ if (uLod.w == 0.0 && (uLod.x != 0.0 || uLod.y != 0.0)) {
1.0 - smoothstep(uLod.y - uLod.z, uLod.y, d)
);
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
float at = 0.0;
// shift by view-offset during multi-sample rendering to allow for blending

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
export const cylinders_frag = `
@@ -120,7 +121,11 @@ bool CylinderImpostor(
#ifdef dSolidInterior
if (interior) cameraNormal = -rayDir;
#endif
return true;
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
return true;
#else
return !interior;
#endif
}
}
} else if (bottomCap && y >= 0.0) {
@@ -146,7 +151,11 @@ bool CylinderImpostor(
#ifdef dSolidInterior
if (interior) cameraNormal = -rayDir;
#endif
return true;
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
return true;
#else
return !interior;
#endif
}
}
}
@@ -258,16 +267,21 @@ void main() {
gl_FragColor = material;
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
mat3 normalMatrix = transpose3(inverse3(mat3(uView)));
vec3 normal = normalize(normalMatrix * -normalize(cameraNormal));
#include apply_light_color
#include apply_interior_color
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normal, emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -69,6 +69,9 @@ uniform float uMetalness;
uniform float uRoughness;
uniform float uEmissive;
// Density value to estimate object thickness
uniform float uDensity;
uniform bool uFog;
uniform float uFogNear;
uniform float uFogFar;
@@ -77,6 +80,7 @@ uniform vec3 uFogColor;
uniform float uAlpha;
uniform bool uTransparentBackground;
uniform float uXrayEdgeFalloff;
uniform float uCelSteps;
uniform float uExposure;
uniform int uRenderMask;
@@ -348,7 +352,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
// TODO: support clipping exclusion texture support
void main() {
#if defined(dRenderVariant_emissive)
#if defined(dRenderVariant_tracing) || defined(dRenderVariant_emissive)
discard;
#else
if (gl_FrontFacing)

View File

@@ -0,0 +1,13 @@
export const accumulate_frag = `
precision highp float;
precision highp sampler2D;
uniform sampler2D tColor;
uniform vec2 uTexSize;
uniform float uWeight;
void main() {
vec2 coords = gl_FragCoord.xy / uTexSize;
gl_FragColor = texture2D(tColor, coords) * uWeight;
}
`;

View File

@@ -0,0 +1,197 @@
export const compose_frag = `
precision highp float;
precision highp sampler2D;
uniform sampler2D tShaded;
uniform sampler2D tColor;
uniform sampler2D tNormal;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform sampler2D tOutlines;
uniform vec2 uTexSize;
uniform float uNear;
uniform float uFar;
uniform float uFogNear;
uniform float uFogFar;
uniform vec3 uFogColor;
uniform vec3 uOutlineColor;
uniform bool uTransparentBackground;
uniform float uDenoiseThreshold;
#include common
float getViewZ(const in float depth) {
#if dOrthographic == 1
return orthographicDepthToViewZ(depth, uNear, uFar);
#else
return perspectiveDepthToViewZ(depth, uNear, uFar);
#endif
}
float getDepthOpaque(const in vec2 coords) {
#ifdef depthTextureSupport
return texture2D(tDepthOpaque, coords).r;
#else
return unpackRGBAToDepth(texture2D(tDepthOpaque, coords));
#endif
}
float getDepthTransparent(const in vec2 coords) {
#ifdef dTransparentOutline
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
#else
return 1.0;
#endif
}
bool isBackground(const in float depth) {
return depth == 1.0;
}
//
// TODO: investigate
// https://interplayoflight.wordpress.com/2022/03/26/raytraced-global-illumination-denoising/
//
#define INV_SQRT_OF_2PI 0.39894228040143267793994605993439 // 1.0/SQRT_OF_2PI
#define INV_PI 0.31830988618379067153776752674503
// https://github.com/BrutPitt/glslSmartDeNoise
//
// smartDeNoise - parameters
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// sampler2D tex - sampler image / texture
// vec2 uv - actual fragment coord
// float sigma > 0 - sigma Standard Deviation
// float kSigma >= 0 - sigma coefficient
// kSigma * sigma --> radius of the circular kernel
// float threshold - edge sharpening threshold
float NormalWeightStrength = 6.0;
vec4 smartDeNoise(sampler2D tex, vec2 uv) {
float sigma = 3.0;
float kSigma = 2.0;
float threshold = uDenoiseThreshold;
vec4 centrPx = texture2D(tex, uv);
if (threshold == 0.0) return centrPx;
float invSigmaQx2 = 0.5 / (sigma * sigma); // 1.0 / (sigma^2 * 2.0)
float invSigmaQx2PI = INV_PI * invSigmaQx2; // 1.0 / (sqrt(PI) * sigma)
float invThresholdSqx2 = 0.5 / (threshold * threshold); // 1.0 / (sigma^2 * 2.0)
float invThresholdSqrt2PI = INV_SQRT_OF_2PI / threshold; // 1.0 / (sqrt(2*PI) * sigma)
float zBuff = 0.0;
vec4 aBuff = vec4(0.0);
vec3 normal = texture2D(tNormal, uv).xyz;
for (int x = -6; x <= 6; ++x) {
for (int y = -6; y <= 6; ++y) {
vec2 d = vec2(float(x), float(y));
float blurFactor = exp(-dot(d , d) * invSigmaQx2) * invSigmaQx2PI;
vec2 uvSample = uv + d / uTexSize;
vec3 normalSample = texture2D(tNormal, uvSample).xyz;
float normalW = saturate(dot(normal, normalSample));
normalW = pow(normalW, NormalWeightStrength);
blurFactor *= normalW;
vec4 walkPx = texture2D(tex, uvSample);
vec4 dC = walkPx - centrPx;
float deltaFactor = exp(-dot(dC, dC) * invThresholdSqx2) * invThresholdSqrt2PI * blurFactor;
zBuff += deltaFactor;
aBuff += deltaFactor * walkPx;
}
}
return aBuff / zBuff;
}
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
float backgroundViewZ = 2.0 * uFar;
vec2 invTexSize = 1.0 / uTexSize;
float transparentDepth = getDepthTransparent(coords);
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
float selfDepth = min(opaqueDepth, transparentDepth);
float outline = 1.0;
closestTexel = 1.0;
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
if (x * x + y * y > dOutlineScale * dOutlineScale) {
continue;
}
vec2 sampleCoords = coords + vec2(float(x), float(y)) * invTexSize;
vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
float sampleOutline = sampleOutlineCombined.r;
float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
outline = 0.0;
closestTexel = sampleOutlineDepth;
}
}
}
return closestTexel < opaqueDepth ? outline : 1.0;
}
void main() {
vec2 coords = gl_FragCoord.xy / uTexSize;
#ifdef dDenoise
vec4 color = smartDeNoise(tColor, coords);
#else
vec4 color = texture2D(tColor, coords);
#endif
float opaqueDepth = getDepthOpaque(coords);
float backgroundViewZ = 2.0 * uFar;
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
float fogFactor = smoothstep(uFogNear, uFogFar, abs(opaqueSelfViewZ));
float fogAlpha = 1.0 - fogFactor;
float alpha = 1.0;
if (!uTransparentBackground) {
// mix opaque objects with background color
color.rgb = mix(color.rgb, uFogColor, fogFactor);
} else {
// pre-multiplied alpha expected for transparent background
alpha = fogAlpha;
color.rgb *= fogAlpha;
}
#ifdef dOutlineEnable
float closestTexel;
float outline = getOutline(coords, opaqueDepth, closestTexel);
if (outline == 0.0) {
float viewDist = abs(getViewZ(closestTexel));
float fogFactorOutline = smoothstep(uFogNear, uFogFar, viewDist);
if (!uTransparentBackground) {
color.rgb = mix(uOutlineColor, uFogColor, fogFactorOutline);
} else {
color.rgb = mix(uOutlineColor, color.rgb, fogFactorOutline);
alpha = 1.0 - fogFactorOutline;
}
}
#endif
gl_FragColor = vec4(color.rgb, alpha);
}
`;

View File

@@ -0,0 +1,373 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export const trace_frag = `
precision highp int;
precision highp float;
precision highp sampler2D;
uniform sampler2D tColor;
uniform sampler2D tNormal;
uniform sampler2D tShaded;
uniform sampler2D tThickness;
uniform sampler2D tAccumulate;
uniform sampler2D tDepth;
uniform vec2 uTexSize;
uniform vec4 uBounds;
uniform float uNear;
uniform float uFar;
uniform float uFogNear;
uniform float uFogFar;
uniform vec3 uFogColor;
#if dLightCount != 0
uniform vec3 uLightDirection[dLightCount];
uniform vec3 uLightColor[dLightCount];
#endif
uniform vec3 uAmbientColor;
uniform vec3 uLightStrength;
uniform int uFrameNo;
uniform float uRayDistance;
uniform float uMinThickness;
uniform float uThicknessFactor;
uniform float uThickness;
uniform float uShadowSoftness;
uniform float uShadowThickness;
uniform mat4 uProjection;
uniform mat4 uInvProjection;
#include common
// parts adapted from
// - https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/
// - https://github.com/0beqz/realism-effects/blob/v2-debug/src/ssgi/shader/ssgi.frag
//
// after a hit, it moves the ray this far along the normal away from a surface.
// Helps prevent incorrect intersections when rays bounce off of objects.
#define RayPosNormalNudge 0.0001
#if __VERSION__ == 100
#define StateType float
// from https://www.shadertoy.com/view/4djSRW
float hash14(vec4 p4) {
p4 = fract(p4 * vec4(0.1031, 0.1030, 0.0973, 0.1099));
p4 += dot(p4, p4.wzxy + 33.33);
return fract((p4.x + p4.y) * (p4.z + p4.w));
}
float randomFloat(inout float state) {
state += 0.06711056;
return 1.0 - hash14(vec4(gl_FragCoord.xy, float(uFrameNo), state));
}
#else
#define StateType uint
// https://www.pcg-random.org/
// https://jcgt.org/published/0009/03/02/
uint pcg(inout uint seed) {
seed = seed * 747796405u + 2891336453u;
uint word = ((seed >> ((seed >> 28u) + 4u)) ^ seed) * 277803737u;
return (word >> 22u) ^ word;
}
float randomFloat(inout uint state) {
return float(pcg(state)) / 4294967296.0;
}
#endif
vec3 randomUnitVector(inout StateType state) {
float z = randomFloat(state) * 2.0 - 1.0;
float a = randomFloat(state) * TWO_PI;
float r = sqrt(1.0 - z * z);
float x = r * cos(a);
float y = r * sin(a);
return vec3(x, y, z);
}
struct RayHitInfo {
bool missed;
vec3 position;
vec3 normal;
vec3 color;
vec3 emissive;
};
//
float getDepth(const in vec2 coords) {
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
return texture2D(tDepth, c).r;
}
float getThickness(const in vec2 coords) {
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
return unpackRGBAToDepth(texture2D(tThickness, c));
}
bool isBackground(const in float depth) {
return depth == 1.0;
}
float getViewZ(const in float depth) {
#if dOrthographic == 1
return orthographicDepthToViewZ(depth, uNear, uFar);
#else
return perspectiveDepthToViewZ(depth, uNear, uFar);
#endif
}
vec2 viewSpaceToScreenSpace(const vec3 position) {
vec4 projectedCoord = uProjection * vec4(position, 1.0);
projectedCoord.xy /= projectedCoord.w;
// [-1, 1] --> [0, 1] (NDC to screen position)
projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
return projectedCoord.xy;
}
vec2 binarySearch(inout vec3 dir, inout vec3 hitPos) {
float rayHitDepthDifference;
vec2 coords;
dir *= 0.5;
hitPos -= dir;
for (int i = 0; i < dRefineSteps; i++) {
coords = viewSpaceToScreenSpace(hitPos);
float depth = getDepth(coords);
float z = getViewZ(depth);
rayHitDepthDifference = z - hitPos.z;
dir *= 0.5;
if (rayHitDepthDifference >= 0.0) {
hitPos -= dir;
} else {
hitPos += dir;
}
}
coords = viewSpaceToScreenSpace(hitPos);
return coords;
}
float calculateGrowthFactor(float begin, float end, float steps) {
return pow(end / begin, 1.0 / steps);
}
vec2 rayMarch(in vec3 dir, in float thickness, inout vec3 hitPos, out bool missed) {
float rayHitDepthDifference;
vec2 coords;
float begin = float(dFirstStepSize);
dir *= begin;
missed = false;
float gf = calculateGrowthFactor(begin, uRayDistance, float(dSteps));
for (int i = 1; i < dSteps; i++) {
hitPos += dir;
dir *= gf;
coords = viewSpaceToScreenSpace(hitPos);
float depth = getDepth(coords);
float z = getViewZ(depth);
rayHitDepthDifference = z - hitPos.z;
if (thickness == 0.0) {
#ifdef dThicknessMode_auto
thickness = max(uMinThickness, (getViewZ(getThickness(coords)) - z) * uThicknessFactor * texture2D(tColor, coords).a);
#else
thickness = uThickness;
#endif
}
if (rayHitDepthDifference >= 0.0 && rayHitDepthDifference < thickness) {
if (dRefineSteps == 0) {
return coords;
} else {
return binarySearch(dir, hitPos);
}
}
}
missed = true;
return coords;
}
//
void trace(in vec3 rayPos, in vec3 rayDir, inout RayHitInfo hitInfo) {
vec3 hitPos = vec3(rayPos);
bool missed;
vec2 coords;
coords = rayMarch(rayDir, 0.0, hitPos, missed);
hitInfo.missed = missed;
hitInfo.position = hitPos;
hitInfo.normal = -texture2D(tNormal, coords).rgb;
hitInfo.color = texture2D(tColor, coords).rgb;
hitInfo.emissive = texture2D(tColor, coords).rgb * texture2D(tNormal, coords).a * 2.0;
float depth = getDepth(coords);
if (isBackground(depth)) {
hitInfo.emissive = vec3(0.0);
}
}
vec3 viewPos;
vec3 colorForRay(in vec3 startRayPos, in vec3 startRayDir, inout StateType rngState) {
vec3 ret = vec3(0.0, 0.0, 0.0);
vec3 throughput = vec3(1.0, 1.0, 1.0);
vec3 rayPos = startRayPos;
vec3 rayDir = startRayDir;
RayHitInfo hitInfo;
RayHitInfo prevHitInfo;
for (int bounceIndex = 0; bounceIndex <= dBounces; ++bounceIndex) {
// shoot a ray out into the world
if (bounceIndex == 0) {
vec2 coords = gl_FragCoord.xy / uTexSize;
float depth = getDepth(coords);
hitInfo.missed = false;
hitInfo.position = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
hitInfo.normal = -texture2D(tNormal, coords).rgb;
hitInfo.color = texture2D(tShaded, coords).rgb;
hitInfo.emissive = texture2D(tColor, coords).rgb * texture2D(tNormal, coords).a;
// shadow
#ifdef dShadowEnable
#if dLightCount != 0
vec3 directLight = vec3(uAmbientColor);
#pragma unroll_loop_start
bool missed;
vec3 hitPos;
for (int i = 0; i < dLightCount; ++i) {
missed = false;
hitPos = viewPos + hitInfo.normal * RayPosNormalNudge;
hitPos += -uLightDirection[i] * (randomFloat(rngState));
rayMarch(-uLightDirection[i] + randomUnitVector(rngState) * uShadowSoftness, uShadowThickness, hitPos, missed);
if (missed) directLight += uLightColor[i];
}
#pragma unroll_loop_end
hitInfo.color *= directLight / uLightStrength;
#endif
#endif
if (hitInfo.normal == vec3(0.0)) {
hitInfo.missed = true;
}
} else {
prevHitInfo = hitInfo;
trace(rayPos, rayDir, hitInfo);
}
// if the ray missed, we are done
if (hitInfo.missed) {
vec3 accIrradiance = vec3(1.0);
#ifdef dGlow
if (bounceIndex > 1) {
accIrradiance = uLightStrength;
}
#else
if (bounceIndex > 1) {
accIrradiance = uAmbientColor;
#if dLightCount != 0
#pragma unroll_loop_start
float dotNL;
vec3 irradiance;
for (int i = 0; i < dLightCount; ++i) {
dotNL = saturate(dot(prevHitInfo.normal, -uLightDirection[i]));
irradiance = dotNL * uLightColor[i];
accIrradiance += irradiance;
}
#pragma unroll_loop_end
#endif
}
#endif
ret += prevHitInfo.color * accIrradiance * throughput;
break;
}
// add emissive light
ret += hitInfo.emissive * throughput;
// update the ray position
rayPos = hitInfo.position + hitInfo.normal * RayPosNormalNudge;
// new ray direction from normal oriented cosine weighted hemisphere sample
rayDir = normalize(hitInfo.normal + randomUnitVector(rngState));
if (bounceIndex == 0) {
continue;
}
// update the colorMultiplier.
throughput *= hitInfo.color;
// Russian Roulette
// As the throughput gets smaller, the ray is more likely to get terminated early.
// Survivors have their value boosted to make up for fewer samples being in the average.
{
float p = max(throughput.r, max(throughput.g, throughput.b));
if (randomFloat(rngState) > p)
break;
// Add the energy we 'lose' by randomly terminating paths
throughput *= 1.0 / p;
}
}
// return pixel color
return ret;
}
void main() {
vec2 coords = gl_FragCoord.xy / uTexSize;
float depth = getDepth(coords);
if (isBackground(depth)) {
gl_FragColor = texture2D(tColor, coords);
return;
}
#if __VERSION__ == 100
float rngState = 26699.0;
#else
// initialize a random number state based on gl_FragCoord and uFrameNo
uint rngState = uint(uint(gl_FragCoord.x) * 1973u + uint(gl_FragCoord.y) * 9277u + uint(uFrameNo) * 26699u) | 1u;
#endif
vec3 cameraPos = vec3(0.0, 0.0, 0.0);
viewPos = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
vec3 rayDir = normalize(viewPos);
// raytrace for this pixel
vec3 color = vec3(0.0, 0.0, 0.0);
for (int index = 0; index < int(dRendersPerFrame); ++index) {
color += colorForRay(cameraPos, rayDir, rngState) / float(dRendersPerFrame);
}
// average the frames together
vec4 lastFrameColor = texture2D(tAccumulate, coords);
float blend = (uFrameNo < 1 || lastFrameColor.a == 0.0) ? 1.0 : 1.0 / (1.0 + (1.0 / lastFrameColor.a));
color = mix(lastFrameColor.rgb, color, blend);
// show the result
gl_FragColor = vec4(color, blend);
}
`;

View File

@@ -12,6 +12,9 @@ precision highp int;
#include common_frag_params
#include common_clip
// Density value to estimate object thickness
uniform float uDensity;
uniform vec2 uImageTexDim;
uniform sampler2D tImageTex;
uniform sampler2D tGroupTex;
@@ -156,7 +159,7 @@ void main() {
}
#elif defined(dRenderVariant_emissive)
gl_FragColor = vec4(0.0);
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
gl_FragColor = imageData;
float marker = uMarker;
@@ -167,9 +170,15 @@ void main() {
}
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normalize(vViewPosition), 0.0);
gl_FragData[2] = vec4(imageData.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -37,13 +37,18 @@ void main(){
gl_FragColor = material;
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
gl_FragColor = material;
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normalize(vViewPosition), emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -53,7 +53,7 @@ void main() {
gl_FragColor = material;
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dFlatShaded)
vec3 normal = -faceNormal;
#else
@@ -61,12 +61,17 @@ void main() {
if (uDoubleSided) normal *= float(frontFacing) * 2.0 - 1.0;
#endif
#include apply_light_color
#include apply_interior_color
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normal, emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -32,7 +32,7 @@ void main(){
if (fuzzyAlpha < 0.0001) discard;
#endif
#if defined(dPointStyle_fuzzy) && defined(dRenderVariant_color)
#if defined(dPointStyle_fuzzy) && (defined(dRenderVariant_color) || defined(dRenderVariant_tracing))
material.a *= fuzzyAlpha;
#endif
@@ -54,13 +54,18 @@ void main(){
gl_FragColor = material;
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
gl_FragColor = material;
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normalize(vViewPosition), emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -13,7 +13,6 @@ precision highp sampler2D;
uniform sampler2D tSsaoDepth;
uniform sampler2D tColor;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform sampler2D tShadows;
uniform sampler2D tOutlines;
uniform vec2 uTexSize;
@@ -46,14 +45,6 @@ float getDepthOpaque(const in vec2 coords) {
#endif
}
float getDepthTransparent(const in vec2 coords) {
#ifdef dTransparentOutline
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
#else
return 1.0;
#endif
}
bool isBackground(const in float depth) {
return depth == 1.0;
}
@@ -62,11 +53,6 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
float backgroundViewZ = 2.0 * uFar;
vec2 invTexSize = 1.0 / uTexSize;
float transparentDepth = getDepthTransparent(coords);
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
float selfDepth = min(opaqueDepth, transparentDepth);
float outline = 1.0;
closestTexel = 1.0;
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
@@ -80,9 +66,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
float sampleOutline = sampleOutlineCombined.r;
float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
outline = 0.0;
closestTexel = sampleOutlineDepth;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -23,13 +23,13 @@ uniform float uFar;
uniform vec3 uLightDirection[dLightCount];
uniform vec3 uLightColor[dLightCount];
#endif
uniform vec3 uAmbientColor;
uniform mat4 uProjection;
uniform mat4 uInvProjection;
uniform float uMaxDistance;
uniform float uTolerance;
uniform float uBias;
bool isBackground(const in float depth) {
return depth == 1.0;
@@ -62,7 +62,7 @@ float screenFade(const in vec2 coords) {
}
// based on https://panoskarabelas.com/posts/screen_space_shadows/
float screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, const in float stepLength) {
vec3 screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, const in vec3 lightColor, const in float stepLength) {
// Ray position and direction (in view-space)
vec3 rayPos = position;
vec3 rayDir = -lightDirection;
@@ -71,7 +71,6 @@ float screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, co
vec3 rayStep = rayDir * stepLength;
// Ray march towards the light
float occlusion = 0.0;
vec4 rayCoords = vec4(0.0);
for (int i = 0; i < dSteps; ++i) {
// Step the ray
@@ -80,8 +79,9 @@ float screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, co
rayCoords = uProjection * vec4(rayPos, 1.0);
rayCoords.xyz = (rayCoords.xyz / rayCoords.w) * 0.5 + 0.5;
if (outsideBounds(rayCoords.xy))
return 1.0;
if (outsideBounds(rayCoords.xy)) {
return lightColor;
}
// Compute the difference between the ray's and the camera's depth
float depth = getDepth(rayCoords.xy);
@@ -89,16 +89,12 @@ float screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, co
float zDelta = rayPos.z - viewZ;
if (zDelta < uTolerance) {
occlusion = 1.0;
// Fade out as we approach the edges of the screen
occlusion *= screenFade(rayCoords.xy);
break;
return mix(vec3(0.0), lightColor, 1.0 - screenFade(rayCoords.xy));
}
}
return 1.0 - (uBias * occlusion);
return lightColor;
}
void main(void) {
@@ -115,17 +111,19 @@ void main(void) {
vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
float stepLength = uMaxDistance / float(dSteps);
float o = 1.0;
float l = length(uAmbientColor);
float a = l;
#if dLightCount != 0
float sh[dLightCount];
vec3 s;
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
sh[i] = screenSpaceShadow(selfViewPos, uLightDirection[i], stepLength);
o = min(o, sh[i]);
s = screenSpaceShadow(selfViewPos, uLightDirection[i], uLightColor[i], stepLength);
l += length(s);
a += length(uLightColor[i]);
}
#pragma unroll_loop_end
#endif
gl_FragColor = vec4(o);
gl_FragColor = vec4(l / a);
}
`;

View File

@@ -72,7 +72,7 @@ bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal
#ifdef dSolidInterior
if (!objectClipped) {
fragmentDepth = 0.0 + (0.0000001 / vRadius);
cameraNormal = -mix(normalize(vPoint), vec3(0.0, 0.0, 1.0), uIsOrtho);
cameraNormal = -mix(normalize(vPoint), vec3(0.0, 0.0, -1.0), uIsOrtho);
}
#endif
return true;
@@ -91,9 +91,9 @@ void main(void){
if (dot(pointDir, pointDir) > vRadius * vRadius) discard;
vec3 vViewPosition = -vPointViewPosition;
fragmentDepth = gl_FragCoord.z;
#if !defined(dIgnoreLight) || defined(dXrayShaded)
pointDir.z -= cos(length(pointDir) / vRadius);
cameraNormal = -normalize(pointDir / vRadius);
#if !defined(dIgnoreLight) || defined(dXrayShaded) || defined(dRenderVariant_tracing)
pointDir.z -= cos(length(pointDir));
cameraNormal = -normalize(pointDir);
#endif
interior = false;
#else
@@ -117,7 +117,7 @@ void main(void){
#endif
#include assign_material_color
#if defined(dRenderVariant_color)
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
if (uRenderMask == MaskTransparent && uAlphaThickness > 0.0) {
material.a *= min(1.0, vRadius / uAlphaThickness);
}
@@ -141,15 +141,20 @@ void main(void){
gl_FragColor = material;
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color)
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
vec3 normal = -cameraNormal;
#include apply_light_color
#include apply_interior_color
#include apply_marker_color
#include apply_fog
#include wboit_write
#include dpoit_write
#if defined(dRenderVariant_color)
#include apply_fog
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normal, emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}
`;

View File

@@ -16,6 +16,7 @@ uniform vec2 uTexSize;
uniform vec4 uBounds;
uniform float uKernel[dOcclusionKernelSize];
uniform float uBlurDepthBias;
uniform float uBlurDirectionX;
uniform float uBlurDirectionY;
@@ -38,6 +39,10 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
bool isNearClip(const in float depth) {
return depth == 0.0;
}
bool outsideBounds(const in vec2 p) {
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
}
@@ -59,16 +64,14 @@ void main(void) {
}
float selfDepth = unpackRGToUnitInterval(packedDepth);
// if background and if second pass
if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
// (if background and if second pass) or if near clip
if ((isBackground(selfDepth) && uBlurDirectionY != 0.0) || isNearClip(selfDepth)) {
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
return;
}
float selfViewZ = getViewZ(selfDepth);
float pixelSize = getPixelSize(coords, selfDepth);
// max diff depth between two pixels
float maxDiffViewZ = 1.0;
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
@@ -86,12 +89,12 @@ void main(void) {
vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
float sampleDepth = unpackRGToUnitInterval(sampleSsaoDepth.zw);
if (isBackground(sampleDepth)) {
if (isBackground(sampleDepth) || isNearClip(sampleDepth)) {
continue;
}
float sampleViewZ = getViewZ(sampleDepth);
if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
if (abs(selfViewZ - sampleViewZ) >= uBlurDepthBias) {
continue;
}

Some files were not shown because too many files have changed in this diff Show More