mirror of
https://github.com/molstar/molstar.git
synced 2026-06-06 14:44:22 +08:00
Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e0d8236c | ||
|
|
fc47276fc3 | ||
|
|
c60334b97b | ||
|
|
36d58d0ff0 | ||
|
|
73529a890b | ||
|
|
b9e88d61a1 | ||
|
|
04bfe71131 | ||
|
|
b16c51825a | ||
|
|
12630dd9f5 | ||
|
|
880b73a3c4 | ||
|
|
63e7ba57bc | ||
|
|
bc2d8a4ce1 | ||
|
|
9f951dbeac | ||
|
|
cba1c23b4d | ||
|
|
d63663a2ea | ||
|
|
41c5ebf1f3 | ||
|
|
757cf0cd13 | ||
|
|
ad8d07cfaa | ||
|
|
e0b307d1a8 | ||
|
|
729306f142 | ||
|
|
8568656d44 | ||
|
|
b7ba8322d1 | ||
|
|
818a0dac0d | ||
|
|
3f96ba92ce | ||
|
|
b356f217ab | ||
|
|
a968fb0984 | ||
|
|
745d8b80d7 | ||
|
|
12ff3aad93 | ||
|
|
e8501b73a5 | ||
|
|
9c07da6de6 | ||
|
|
8c2e58b67c | ||
|
|
80d7649dbb | ||
|
|
6e63bb4283 | ||
|
|
ba7a4137fe | ||
|
|
2ca0a4291b | ||
|
|
32a1a35a96 | ||
|
|
df129d8ce3 | ||
|
|
c346da9f6d | ||
|
|
de15a3d05d | ||
|
|
392b42f6f0 | ||
|
|
46a9b587b4 | ||
|
|
3334a636c5 | ||
|
|
94d52dddda | ||
|
|
b35a73b50f | ||
|
|
7fde6a810d | ||
|
|
843eae1e49 | ||
|
|
8513183684 | ||
|
|
790bebf302 | ||
|
|
0fb76261e8 | ||
|
|
53c69640b7 | ||
|
|
d70cef8ad3 | ||
|
|
a84a23cbcc | ||
|
|
736f2dc657 | ||
|
|
06295fd586 | ||
|
|
e9f4d95dc3 | ||
|
|
223e3b6fbf | ||
|
|
16c967b674 | ||
|
|
a6a1f0621e | ||
|
|
60cb722343 | ||
|
|
2569fe9577 | ||
|
|
be717133ef | ||
|
|
231d585236 | ||
|
|
098faf129c | ||
|
|
0b39ad8341 | ||
|
|
c0117c41e6 | ||
|
|
0ce41e989a | ||
|
|
b6885a0d76 | ||
|
|
125120fcab | ||
|
|
2147a5c3fb | ||
|
|
8c7d5b9585 | ||
|
|
aa4c36885d | ||
|
|
4ee4788378 | ||
|
|
47aea2b12f | ||
|
|
490bc82ee6 | ||
|
|
0d24c636a3 | ||
|
|
5a81b4f375 | ||
|
|
73b90ffb5c | ||
|
|
02e795b265 | ||
|
|
325aa74331 | ||
|
|
1efe2eb329 | ||
|
|
1e895f3c8c | ||
|
|
028c283043 | ||
|
|
144ed51100 | ||
|
|
e3c2ec4561 | ||
|
|
84dd957983 | ||
|
|
1093a4f6ad | ||
|
|
c4fdc43aa0 | ||
|
|
15da722af5 | ||
|
|
eec2d2a720 | ||
|
|
375db11e9b | ||
|
|
b1b1972684 | ||
|
|
ce0d4cbc4e | ||
|
|
127d9bc94e | ||
|
|
860df1a898 | ||
|
|
51b36e90f0 | ||
|
|
48b19e149b | ||
|
|
5a87d9dbf5 | ||
|
|
c07b4ba550 | ||
|
|
8a99e3e3fd | ||
|
|
571f54f4e6 | ||
|
|
15cd7b9c13 | ||
|
|
0d21b399b5 | ||
|
|
94ad0bf75c | ||
|
|
2c44286ca5 | ||
|
|
23705727ac | ||
|
|
0a173d230c | ||
|
|
f8987af0e8 | ||
|
|
e046b80bf2 | ||
|
|
f8d6f1d010 | ||
|
|
579190b9ce | ||
|
|
e44e29eb9f | ||
|
|
589cec24e5 | ||
|
|
fd999953f9 | ||
|
|
523dfe7928 | ||
|
|
b2f26e6b1d | ||
|
|
dc45bf3915 | ||
|
|
96e22e25cf | ||
|
|
051beb3c3c | ||
|
|
2ba3d67520 | ||
|
|
cd30d9c1a3 | ||
|
|
7d32aa8276 | ||
|
|
f837b46da1 | ||
|
|
c6107ff694 | ||
|
|
2e7228f88b | ||
|
|
e8825eac5d | ||
|
|
1a88126af8 | ||
|
|
c4a6eba448 | ||
|
|
fc7e9501b2 | ||
|
|
1dfd52db43 | ||
|
|
5510b28656 | ||
|
|
e94abdb159 | ||
|
|
7015607244 | ||
|
|
7ff37d7dcc | ||
|
|
3abc2da106 | ||
|
|
f9c498177a | ||
|
|
872c6483be | ||
|
|
53288e4e9d | ||
|
|
d6b045594c | ||
|
|
aa86111de7 | ||
|
|
040473388e | ||
|
|
f474615729 | ||
|
|
92559e456e | ||
|
|
b2434ea0d0 | ||
|
|
6cf0ce5574 | ||
|
|
518a40f0ba | ||
|
|
387e87bfda | ||
|
|
4fac2a5cd6 | ||
|
|
f5f3ea84d4 | ||
|
|
4b3d470dde | ||
|
|
8513a44e8c | ||
|
|
84b54d97df | ||
|
|
34606f258e | ||
|
|
2c10dd46a0 | ||
|
|
d4c80fc995 | ||
|
|
e1c00f65a5 | ||
|
|
012bc9e8e8 | ||
|
|
a99083107c | ||
|
|
7e93bb0dda | ||
|
|
9735cce043 | ||
|
|
78e1d76f5e | ||
|
|
18b1492d54 | ||
|
|
6116b2fea5 | ||
|
|
6ef8fd2b64 | ||
|
|
9319805d36 | ||
|
|
5027ad37d7 | ||
|
|
70bd0c25c4 | ||
|
|
1a5c7f5437 | ||
|
|
4a9505c334 | ||
|
|
b43ec9ed45 | ||
|
|
eb9c6d542b | ||
|
|
2ec0911821 | ||
|
|
bbb34c8a27 | ||
|
|
1bcb8d6486 | ||
|
|
630b5ca203 | ||
|
|
5b2ed784e1 | ||
|
|
11c9a83ee7 | ||
|
|
2d1b61647a | ||
|
|
fa3797a738 | ||
|
|
fc60c0c980 | ||
|
|
372ca20980 | ||
|
|
b0aad9f1ff | ||
|
|
40e45adbb0 | ||
|
|
5b43a2cee9 | ||
|
|
a319a0daa8 | ||
|
|
79f812d0e1 | ||
|
|
45611a25a5 | ||
|
|
77be659915 | ||
|
|
5986250ed9 | ||
|
|
8156c672b0 | ||
|
|
a443512102 | ||
|
|
4b921319a8 | ||
|
|
6329820a87 | ||
|
|
91e4b0c3d6 | ||
|
|
7666617857 | ||
|
|
19be1090b3 | ||
|
|
354438052e | ||
|
|
b72444b213 | ||
|
|
179078f45c | ||
|
|
1dbc23fe91 | ||
|
|
ff4dec9fea | ||
|
|
6ea51c07b4 | ||
|
|
f4cebb9195 | ||
|
|
cbe5f0dc7c | ||
|
|
0ac8b565b5 | ||
|
|
4f38d4d943 | ||
|
|
0af84eb6b5 | ||
|
|
aa2d19478b | ||
|
|
e035b834a6 | ||
|
|
2b2dfd9245 | ||
|
|
678790efa3 | ||
|
|
a121c5e2cd | ||
|
|
47b242244e | ||
|
|
2b9d3fd33a | ||
|
|
81404036a2 | ||
|
|
e2dc15cf0f | ||
|
|
365a91879f | ||
|
|
e47e0eb51a | ||
|
|
390046e38f | ||
|
|
b8eb5191a2 | ||
|
|
4cc416ca28 | ||
|
|
f4b2458390 | ||
|
|
1cad6eef74 | ||
|
|
2923be6006 | ||
|
|
70959641a1 | ||
|
|
051608f56c | ||
|
|
71c1a4e85b | ||
|
|
ba06c9e413 | ||
|
|
8e4dfd1ffd | ||
|
|
fac8aa529f | ||
|
|
d35b4b5e62 | ||
|
|
dd1789478b | ||
|
|
af27a00a01 | ||
|
|
17cea8f99c | ||
|
|
4f6d5a7dc7 | ||
|
|
857972653e | ||
|
|
98ff0f5c55 | ||
|
|
43803a91ea | ||
|
|
8a1bab8bcb | ||
|
|
17a47faaff | ||
|
|
f793167e91 | ||
|
|
d1c2c8e837 | ||
|
|
5d7ef8196e | ||
|
|
e0715cbf5c | ||
|
|
1af8522de3 | ||
|
|
c6becd5741 | ||
|
|
0ca368f29f | ||
|
|
5039a448ad | ||
|
|
9ef38f02c9 | ||
|
|
66f4ff1140 | ||
|
|
cc077656a9 | ||
|
|
3ef1a2ec0a | ||
|
|
144bf6954e | ||
|
|
50e5538148 | ||
|
|
efe95f92c7 | ||
|
|
09f858a755 | ||
|
|
e7082d4ccc | ||
|
|
732a8f4bd0 | ||
|
|
82ca06b29e | ||
|
|
a05429f13f | ||
|
|
377de7ad40 | ||
|
|
74f4d00c8d | ||
|
|
be3825372e | ||
|
|
d62c5c9050 | ||
|
|
9ba5112beb | ||
|
|
048658ee39 | ||
|
|
2918081dd9 | ||
|
|
1a67868c07 | ||
|
|
525dfaddd2 | ||
|
|
8aa12c0d31 | ||
|
|
8cb464a686 | ||
|
|
e0371d7e32 | ||
|
|
e7da6bc194 | ||
|
|
933869b5e1 | ||
|
|
5d6adc46fe | ||
|
|
b8a98efcaf | ||
|
|
53b358f70a | ||
|
|
bce9e5b0ad | ||
|
|
cf7f9d6aba | ||
|
|
6af1bc5def | ||
|
|
936808e271 | ||
|
|
8b985d0424 | ||
|
|
b290cf121a | ||
|
|
89df6cec42 | ||
|
|
fc5fc7fcdb | ||
|
|
a1de5bb304 | ||
|
|
25c8a41e91 | ||
|
|
959249b572 | ||
|
|
44610b8b1a | ||
|
|
4070453209 | ||
|
|
cf2193f4fc | ||
|
|
bfc0a3d1fe | ||
|
|
53a2155d8c | ||
|
|
f22121521b | ||
|
|
755655d067 | ||
|
|
41ab186fd2 | ||
|
|
9039c653cb | ||
|
|
dde3f4ecff | ||
|
|
64cd05cc14 | ||
|
|
593e8f4993 | ||
|
|
d2d9eb622f | ||
|
|
ce9883517f | ||
|
|
bc2afe1d68 | ||
|
|
5a2ee03b48 | ||
|
|
2d86c76788 | ||
|
|
7f8995a4d8 | ||
|
|
64ab8bf78d | ||
|
|
bf9663e177 | ||
|
|
fb729446e2 | ||
|
|
3e4082bf6e | ||
|
|
7148c7197b | ||
|
|
d2192d609a | ||
|
|
46ad8f495f | ||
|
|
65b2b69a64 | ||
|
|
0d9d173ef4 | ||
|
|
6eaf8e1911 | ||
|
|
3220ab6118 | ||
|
|
5c882f1aa5 | ||
|
|
2320518b87 |
112
CHANGELOG.md
112
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
193
docs/docs/plugin/custom-library.md
Normal file
193
docs/docs/plugin/custom-library.md
Normal 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
|
||||
|
||||

|
||||
|
||||
## 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`:
|
||||

|
||||
BIN
docs/docs/plugin/lib-example.png
Normal file
BIN
docs/docs/plugin/lib-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
BIN
docs/docs/plugin/ui-example.png
Normal file
BIN
docs/docs/plugin/ui-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
@@ -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
1822
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
79
src/apps/mesoscale-explorer/embedded.html
Normal file
79
src/apps/mesoscale-explorer/embedded.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'That’s 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>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
667
src/mol-canvas3d/passes/illumination.ts
Normal file
667
src/mol-canvas3d/passes/illumination.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
],
|
||||
|
||||
147
src/mol-canvas3d/passes/outline.ts
Normal file
147
src/mol-canvas3d/passes/outline.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
170
src/mol-canvas3d/passes/shadow.ts
Normal file
170
src/mol-canvas3d/passes/shadow.ts
Normal 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);
|
||||
}
|
||||
545
src/mol-canvas3d/passes/ssao.ts
Normal file
545
src/mol-canvas3d/passes/ssao.ts
Normal 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;
|
||||
}
|
||||
508
src/mol-canvas3d/passes/tracing.ts
Normal file
508
src/mol-canvas3d/passes/tracing.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -76,6 +76,7 @@ uniform vec3 uInteriorColor;
|
||||
bool interior;
|
||||
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uCelSteps;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform mat4 uProjection;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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)
|
||||
|
||||
13
src/mol-gl/shader/illumination/accumulate.frag.ts
Normal file
13
src/mol-gl/shader/illumination/accumulate.frag.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
197
src/mol-gl/shader/illumination/compose.frag.ts
Normal file
197
src/mol-gl/shader/illumination/compose.frag.ts
Normal 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);
|
||||
}
|
||||
`;
|
||||
373
src/mol-gl/shader/illumination/trace.frag.ts
Normal file
373
src/mol-gl/shader/illumination/trace.frag.ts
Normal 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);
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user