mirror of
https://github.com/molstar/molstar.git
synced 2026-06-06 22:54:22 +08:00
Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f10d0cb5 | ||
|
|
aed1056d6c | ||
|
|
be47ac09c9 | ||
|
|
d5e7797a40 | ||
|
|
0aeac628c7 | ||
|
|
668d617cd7 | ||
|
|
62ed993f0d | ||
|
|
aa0a008a41 | ||
|
|
89f01f202d | ||
|
|
733190f7a0 | ||
|
|
50429aacfa | ||
|
|
fa541bdbd3 | ||
|
|
77d173afed | ||
|
|
a934001ae8 | ||
|
|
e5d4606437 | ||
|
|
fb16cd0070 | ||
|
|
c427549b8d | ||
|
|
310300bde8 | ||
|
|
11604b9e8f | ||
|
|
cc1bf482f2 | ||
|
|
61a351b3d4 | ||
|
|
9e91a242bf | ||
|
|
c3daa1a162 | ||
|
|
fe086fb62e | ||
|
|
c2217829a3 | ||
|
|
6333c8073f | ||
|
|
2801bcf111 | ||
|
|
8a2461e157 | ||
|
|
0a081e2a8a | ||
|
|
700a3fe95c | ||
|
|
febc634d8b | ||
|
|
0105f75bb6 | ||
|
|
4cc2073eaa | ||
|
|
9ac204cb6e | ||
|
|
73378bbe9d | ||
|
|
9b5fd2595c | ||
|
|
bca2073ed0 | ||
|
|
f264e4d6b8 | ||
|
|
795222b5b4 | ||
|
|
25eb4450ad | ||
|
|
140df13dae | ||
|
|
792cd513a8 | ||
|
|
14e6172c33 | ||
|
|
911433e056 | ||
|
|
6b585cf0d6 | ||
|
|
86211aaf3a | ||
|
|
071623f5b6 | ||
|
|
21e514ec1e | ||
|
|
9bc0ab12e7 | ||
|
|
1d1bd05400 | ||
|
|
faa750bbf9 | ||
|
|
e92e5c5cef | ||
|
|
b49230ea1f | ||
|
|
44ebc1d39a | ||
|
|
8d8e45f4ce | ||
|
|
898d877aa1 | ||
|
|
85dba9b1a4 | ||
|
|
6b5e90c5fa | ||
|
|
e231fbf3d7 | ||
|
|
0ee8525b2d | ||
|
|
106ee614e7 | ||
|
|
34056751f9 | ||
|
|
1afea8a86a | ||
|
|
96d5bf2447 | ||
|
|
f9265a7049 | ||
|
|
5c57137890 | ||
|
|
4e71618d0f | ||
|
|
de660cc233 | ||
|
|
616a1dabfa | ||
|
|
46ea39703f | ||
|
|
6cf20d0c44 | ||
|
|
0737e23b70 | ||
|
|
70d0c15d28 | ||
|
|
9272c8c5ec | ||
|
|
a3349f82fc | ||
|
|
4d399edbdd | ||
|
|
64598eba96 | ||
|
|
aa25874775 | ||
|
|
dccc06d497 | ||
|
|
c000526cf8 | ||
|
|
2166ab455c | ||
|
|
4de9ce01fc | ||
|
|
f543fd5683 | ||
|
|
8535013ee5 | ||
|
|
320ab77f8e | ||
|
|
982feef0c6 | ||
|
|
bd6d04cefb | ||
|
|
5e1c351efc | ||
|
|
61a294c889 | ||
|
|
71fbd6baab | ||
|
|
33430a836a | ||
|
|
f428e9f39e | ||
|
|
2d26425cbe | ||
|
|
f6030aee25 | ||
|
|
609e03f7d2 | ||
|
|
ba12a8bbee | ||
|
|
947f293844 | ||
|
|
fbff0e769c | ||
|
|
3798223d39 | ||
|
|
ac9c23dc65 | ||
|
|
096f492ccb | ||
|
|
ba96da9354 | ||
|
|
6c1d17bac5 | ||
|
|
ad2ccf4e07 | ||
|
|
dc1b7b4693 | ||
|
|
59e4e2b31d | ||
|
|
d2483dc449 | ||
|
|
d26946e9ee | ||
|
|
cd045a6b48 | ||
|
|
2407729d27 | ||
|
|
1aa22b9fa0 | ||
|
|
35c9f39a69 | ||
|
|
7dd420cc18 | ||
|
|
1d434c259a | ||
|
|
6d193edd68 | ||
|
|
9bf859d6ed | ||
|
|
207230d565 | ||
|
|
b7a673f38e | ||
|
|
2204e4e0d0 | ||
|
|
6276365766 | ||
|
|
505b04c92d | ||
|
|
fc84dcb037 | ||
|
|
2f29ff7314 | ||
|
|
b37f043876 | ||
|
|
f0e725f65c | ||
|
|
23a34e2df1 | ||
|
|
d11e242b70 | ||
|
|
d9af0ca068 | ||
|
|
b7f10acbf0 | ||
|
|
43749ccdbd | ||
|
|
3bf4a8f8e6 | ||
|
|
f0ae1b3347 | ||
|
|
99809d25b9 | ||
|
|
e83c0af67c | ||
|
|
2ddf94313e | ||
|
|
da5965c956 | ||
|
|
31be0af3c9 | ||
|
|
38c550b245 | ||
|
|
95a7a2cef9 | ||
|
|
1a1ec51736 | ||
|
|
299aae56c1 | ||
|
|
781824c961 | ||
|
|
930cfa2590 | ||
|
|
35439f01aa | ||
|
|
71121e52af | ||
|
|
c08155717f | ||
|
|
de4164e7a4 | ||
|
|
e34d1242a9 | ||
|
|
5b82641018 | ||
|
|
de2f0c27b2 | ||
|
|
71e2afe781 | ||
|
|
3cba621fcf | ||
|
|
d79a2077c1 | ||
|
|
6925547b5f | ||
|
|
84aae8cf0a | ||
|
|
bdb42e39ec | ||
|
|
6edd54ee6d | ||
|
|
198f884d8b | ||
|
|
2c7d0a6721 | ||
|
|
7ef15ede0d | ||
|
|
3d96298b55 | ||
|
|
964f045e56 | ||
|
|
d3364ac109 | ||
|
|
a5b963c919 | ||
|
|
d6fcbbf543 | ||
|
|
f2e7e2eaf2 | ||
|
|
01c4c63114 | ||
|
|
22f9bc4ff1 | ||
|
|
c6c4350638 | ||
|
|
a17a0c4527 | ||
|
|
8e507012c1 | ||
|
|
beb4351dc9 | ||
|
|
afbb940721 | ||
|
|
f5c619a4c7 | ||
|
|
1b0401dff5 | ||
|
|
649e779100 | ||
|
|
f61e0e72a8 | ||
|
|
803f75fdde | ||
|
|
718d314eda | ||
|
|
adab6b0a6a | ||
|
|
d295ed2eca | ||
|
|
d18cbfa8cf | ||
|
|
59f881c4be | ||
|
|
0295e0ef63 | ||
|
|
dcdb95a055 | ||
|
|
e379d27722 | ||
|
|
41fbe0d2b7 | ||
|
|
1231666b06 | ||
|
|
b302bb8455 | ||
|
|
6e82405600 | ||
|
|
a678893bdb | ||
|
|
430348a3cd | ||
|
|
315401c166 | ||
|
|
b309c545f5 | ||
|
|
60b5d2d39b | ||
|
|
eb749a2a16 | ||
|
|
6db96001a3 | ||
|
|
257370ad58 | ||
|
|
557bf63b55 | ||
|
|
0e32e0a785 | ||
|
|
f2c607a4b2 | ||
|
|
0d12a9e118 | ||
|
|
02f418c8c5 | ||
|
|
ba618c9e4a | ||
|
|
ebd3ebe7b2 | ||
|
|
804117475b | ||
|
|
09ab8d6219 | ||
|
|
f3a5369690 | ||
|
|
8bf2fe624d | ||
|
|
50c1b667c5 | ||
|
|
360031d37c | ||
|
|
9ec873e0db | ||
|
|
c830a720b0 | ||
|
|
1aa7d1e0f7 | ||
|
|
c5c8de8628 | ||
|
|
74c6d6f5a1 | ||
|
|
2bff0faff7 | ||
|
|
4df028aa77 | ||
|
|
47c2d153aa | ||
|
|
18be09e9d5 | ||
|
|
55e940e88c | ||
|
|
e246f4e5ca | ||
|
|
5e1bb4b106 | ||
|
|
0b2889bb99 | ||
|
|
2994caf411 | ||
|
|
e157993a0f | ||
|
|
6c7c9afc34 | ||
|
|
2d0b17d93c | ||
|
|
033c613c89 | ||
|
|
1985eb59dd | ||
|
|
1cf6cbf8a3 | ||
|
|
0b42379c34 | ||
|
|
414c349974 | ||
|
|
cf6d5f7194 | ||
|
|
949f5207b4 | ||
|
|
a1da374b32 | ||
|
|
5460322d4a | ||
|
|
8b2da0b787 | ||
|
|
3eaf4dacaf | ||
|
|
d66d9b4dd7 | ||
|
|
cc52279e01 | ||
|
|
0def474f6d | ||
|
|
e0ea9a2855 | ||
|
|
2bc381fe05 | ||
|
|
fb3cd3bf52 | ||
|
|
c4414c7cc4 | ||
|
|
e2f2ceb7a9 | ||
|
|
641e7efb11 | ||
|
|
11f2ef50ef | ||
|
|
869ecfaf71 | ||
|
|
cb8731815c | ||
|
|
a9177ad362 | ||
|
|
ad116df73b | ||
|
|
f30b3a410c | ||
|
|
c440ba2d4b | ||
|
|
a3267dafdb | ||
|
|
7a1e83733c | ||
|
|
7cb96ce983 | ||
|
|
a73633d0c3 | ||
|
|
b2f8e8dd4e | ||
|
|
291d7abb78 | ||
|
|
32873d787b | ||
|
|
e243d71abf | ||
|
|
2689d3f21a | ||
|
|
c1bc008114 | ||
|
|
254578460a | ||
|
|
f5467dd3b9 | ||
|
|
9eb8714e11 | ||
|
|
847678ea56 | ||
|
|
f08729a402 | ||
|
|
a7c91257a7 | ||
|
|
835369a91e | ||
|
|
62554b522f | ||
|
|
fd041cd4c3 | ||
|
|
cfbb68c8ef | ||
|
|
d7acec4f7d | ||
|
|
7da46bca8b | ||
|
|
66b4fcdc2c | ||
|
|
c480579ca8 | ||
|
|
00ff1a1eae | ||
|
|
ae795f8ad3 | ||
|
|
9d3c071689 | ||
|
|
01cb23f566 | ||
|
|
fe8a9799ab | ||
|
|
4f18154681 | ||
|
|
2114c4a3ad | ||
|
|
2ca41b2b51 | ||
|
|
6605a2019e | ||
|
|
8b1ed5f183 | ||
|
|
f11a1b788f | ||
|
|
7928e24c54 | ||
|
|
5dbca41da6 | ||
|
|
f3fa54addf | ||
|
|
e636397f90 | ||
|
|
1f3e20704d | ||
|
|
cc9bdd4f14 | ||
|
|
6d76bf120d | ||
|
|
a50e81551f | ||
|
|
86512bcea1 | ||
|
|
975f45eb01 | ||
|
|
f2399d3179 | ||
|
|
b26d62a067 | ||
|
|
926d6cbd46 | ||
|
|
7ea47d2a99 | ||
|
|
89ad8cfc15 | ||
|
|
302a309aff | ||
|
|
fbc74c0012 | ||
|
|
27a953795c | ||
|
|
c3e62bc2e5 | ||
|
|
c2ab322bd2 | ||
|
|
aeab0f235c | ||
|
|
ae2285599f | ||
|
|
104ab757d2 | ||
|
|
6ada52bc0b | ||
|
|
c526cb9f08 | ||
|
|
a1662d76fb | ||
|
|
de84a8c8c5 | ||
|
|
4fa135daf0 | ||
|
|
9870cb4082 | ||
|
|
b2924761ab | ||
|
|
509e6bc2d8 |
168
CHANGELOG.md
168
CHANGELOG.md
@@ -6,6 +6,174 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Do not call `updateFocusRepr` if default `StructureFocusRepresentation` isn't present.
|
||||
- Treat "tap" as a click in `InputObserver`
|
||||
- ModelServer ligand queries: fix atom count reported by SDF/MOL/MOL2 export
|
||||
- CCD extension: Make visuals for aromatic bonds configurable
|
||||
- Add optional `file?: CifFile` to `MmcifFormat.data`
|
||||
- Add support for webgl extensions
|
||||
- `WEBGL_clip_cull_distance`
|
||||
- `EXT_conservative_depth`
|
||||
- `WEBGL_stencil_texturing`
|
||||
- `EXT_clip_control`
|
||||
- Add `MultiSampleParams.reduceFlicker` (to be able to switch it off)
|
||||
- Add `alphaThickness` parameter to adjust alpha of spheres for radius
|
||||
- Ability to hide "right" panel from simplified viewport controls
|
||||
- Add `blockIndex` parameter to TrajectoryFromMmCif
|
||||
- Fix bounding sphere calculation for "element-like" visuals
|
||||
- Fix RCSB PDB validation report URL
|
||||
- Add sharpening postprocessing option
|
||||
- Take pixel-ratio into account for outline scale
|
||||
- Gracefully handle missing HTMLImageElement
|
||||
- Fix pixel-ratio changes not applied to all render passes
|
||||
|
||||
## [v3.39.0] - 2023-09-02
|
||||
|
||||
- Add some elements support for `guessElementSymbolString` function
|
||||
- Faster bounding rectangle calculation for imposter spheres
|
||||
- Allow toggling of hydrogens as part of `LabelTextVisual`
|
||||
|
||||
## [v3.38.3] - 2023-07-29
|
||||
|
||||
- Fix imposter spheres not updating, e.g. in trajectories (broke in v3.38.0)
|
||||
|
||||
## [v3.38.2] - 2023-07-24
|
||||
|
||||
- Don't rely solely on `chem_comp_atom` when detecting CCD files (#877)
|
||||
- Actually support non-physical keys in `Bindings.Trigger.code`
|
||||
|
||||
## [v3.38.1] - 2023-07-22
|
||||
|
||||
- Fix pixel-scale not updated in SSAO pass
|
||||
|
||||
## [v3.38.0] - 2023-07-18
|
||||
|
||||
- Fix display issue with SIFTS mapping
|
||||
- Support non-physical keys in `Bindings.Trigger.code`
|
||||
- Update `getStateSnapshot` to only overwrite current snapshot if it was created automatically
|
||||
- Fix distinct palette's `getSamples` infinite loop
|
||||
- Add 'NH2', 'FOR', 'FMT' to `CommonProteinCaps`
|
||||
- Add `opened` event to `PluginStateSnapshotManager`
|
||||
- Properly switch-off fog
|
||||
- Add `approximate` option for spheres rendering
|
||||
- Reduce `Spheres` memory usage
|
||||
- Derive mapping from VertexID
|
||||
- Pull position and group from texture
|
||||
- Add `Euler` math primitive
|
||||
- Add stride option to element sphere & point visuals
|
||||
- Add `disabledExtensions` field to default viewer's options
|
||||
- Add `LRUCache.remove`
|
||||
- Add 'Chain Instance' and 'Uniform' options for 'Carbon Color' param (in Color Theme: Element Symbol)
|
||||
|
||||
## [v3.37.1] - 2023-06-20
|
||||
|
||||
- Fix issues with wboit/dpoit in large scenes
|
||||
- Fix lines, text, points rendering (broken in v3.37.0)
|
||||
|
||||
## [v3.37.0] - 2023-06-17
|
||||
|
||||
- Add `inverted` option to `xrayShaded` parameter
|
||||
- Model-export extension: Add ability to set a file name for structures
|
||||
- Add `contextHash` to `SizeTheme`
|
||||
- Add mipmap-based blur for image backgrounds
|
||||
|
||||
## [v3.36.1] - 2023-06-11
|
||||
|
||||
- Allow parsing of CCD ligand files
|
||||
- Add dedicated wwPDB CCD extension to align and visualize ideal & model CCD coordinates
|
||||
- Make operators in `IndexPairBonds` a directed property
|
||||
- Remove erroneous bounding-box overlap test in `Structure.eachUnitPair`
|
||||
- Fix `EdgeBuilder.addNextEdge` for loop edges
|
||||
- Optimize inter unit bond compute
|
||||
- Ensure consistent state for volume representation (#210)
|
||||
- Improve SSAO for thin geometry (e.g. lines)
|
||||
- Add snapshot support for structure selections
|
||||
- Add `nucleicProfile` parameter to cartoon representation
|
||||
- Add `cartoon` theme with separate colorings for for mainchain and sidechain visuals
|
||||
|
||||
## [v3.35.0] - 2023-05-14
|
||||
|
||||
- Enable odd dash count (1,3,5)
|
||||
- Add principal axes spec and fix edge cases
|
||||
- Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
|
||||
- Mesh exporter improvements
|
||||
- Support points & lines in glTF export
|
||||
- Set alphaMode and doubleSided in glTF export
|
||||
- Fix flipped cylinder caps
|
||||
- Fix bond assignments `struct_conn` records referencing waters
|
||||
- Add StructConn extension providing functions for inspecting struct_conns
|
||||
- Fix `PluginState.setSnapshot` triggering unnecessary state updates
|
||||
- Fix an edge case in the `mol-state`'s `State` when trying to apply a transform to an existing Null object
|
||||
- Add `SbNcbrPartialCharges` extension for coloring and labeling atoms and residues by partial atomic charges
|
||||
- uses custom mmcif categories `_sb_ncbr_partial_atomic_charges_meta` and `_sb_ncbr_partial_atomic_charges` (more info in [README.md](./src/extensions/sb-ncbr/README.md))
|
||||
- Parse HEADER record when reading PDB file
|
||||
- Support `ignoreHydrogens` in interactions representation
|
||||
- Add hydroxyproline (HYP) commonly present in collagen molecules to the list of amino acids
|
||||
- Fix assemblies for Archive PDB files (do not generate unique `label_asym_id` if `REMARK 350` is present)
|
||||
- Add additional functions to `core.math` in `mol-script`
|
||||
- `cantorPairing`, `sortedCantorPairing`, `invertCantorPairing`,
|
||||
- `trunc`, `sign`
|
||||
|
||||
## [v3.34.0] - 2023-04-16
|
||||
|
||||
- Avoid `renderMarkingDepth` for fully transparent renderables
|
||||
- Remove `camera.far` doubling workaround
|
||||
- Add `ModifiersKeys.areNone` helper function
|
||||
- Do not render NtC tube segments unless all required atoms are present in the structure
|
||||
- Fix rendering issues caused by VAO reuse
|
||||
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
|
||||
- Improve trackball move-state handling when key bindings use modifiers
|
||||
- Fix rendering with very small viewport and SSAO enabled
|
||||
- Fix `.getAllLoci` for structure representations with `structure.child`
|
||||
- Fix `readAllLinesAsync` refering to dom length property
|
||||
- Make mol-util/file-info node compatible
|
||||
- Add `eachLocation` to representation/visual interface
|
||||
|
||||
## [v3.33.0] - 2023-04-02
|
||||
|
||||
- Handle resizes of viewer element even when window remains the same size
|
||||
- Throttle canvas resize events
|
||||
- Selection toggle buttons hidden if selection mode is off
|
||||
- Camera focus loci bindings allow reset on click-away to be overridden
|
||||
- Input/controls improvements
|
||||
- Move or fly around the scene using keys
|
||||
- Pointer lock to look around scene
|
||||
- Toggle spin/rock animation using keys
|
||||
- Apply bumpiness as lightness variation with `ignoreLight`
|
||||
- Remove `JSX` reference from `loci-labels.ts`
|
||||
- Fix overpaint/transparency/substance smoothing not updated when geometry changes
|
||||
- Fix camera project/unproject when using offset viewport
|
||||
- Add support for loading all blocks from a mmcif file as a trajectory
|
||||
- Add `Frustum3D` and `Plane3D` math primitives
|
||||
- Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
|
||||
- Remove LazyImports (introduced in v3.31.1)
|
||||
|
||||
## [v3.32.0] - 2023-03-20
|
||||
|
||||
- Avoid rendering of fully transparent renderables
|
||||
- Add occlusion color parameter
|
||||
- Fix issue with outlines and orthographic camera
|
||||
- Reduce over-blurring occlusion at larger view distances
|
||||
- Fix occlusion artefact with non-canvas viewport and pixel-ratio > 1
|
||||
- Update nodejs-shims conditionals to handle polyfilled document object in NodeJS environment.
|
||||
- Ensure marking edges are at least one pixel wide
|
||||
- Add exposure parameter to renderer
|
||||
- Only trigger marking when mouse is directly over canvas
|
||||
- Fix blurry occlusion in screenshots
|
||||
- [Breaking] Add `setFSModule` to `mol-util/data-source` instead of trying to trick WebPack
|
||||
|
||||
## [v3.31.4] - 2023-02-24
|
||||
|
||||
- Allow link cylinder/line `dashCount` set to '0'
|
||||
- Stop animation loop when disposing `PluginContext` (thanks @gfrn for identifying the issue)
|
||||
|
||||
## [v3.31.3] - 2023-02-22
|
||||
|
||||
- Fix impostor bond visuals not correctly updating on `sizeFactor` changes
|
||||
- Fix degenerate case in PCA
|
||||
- Fix near clipping avoidance in impostor shaders
|
||||
- Update `fs` import in `data-source.ts`
|
||||
|
||||
## [v3.31.2] - 2023-02-12
|
||||
|
||||
- Fix exit code of volume pack executable (pack.ts). Now exits with non-0 status when an error happens
|
||||
|
||||
118
docs/extensions/struct-conn.md
Normal file
118
docs/extensions/struct-conn.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# wwPDB StructConn extension
|
||||
|
||||
The STRUCT_CONN category in the mmCIF file format contains details about the connections between portions of the structure. These can be hydrogen bonds, salt bridges, disulfide bridges and so on (see more at <https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v40.dic/Categories/struct_conn.html>).
|
||||
|
||||
**wwPDB StructConn extension** in Mol* provides functionality to retrieve and visualize these connections.
|
||||
|
||||
The extension exposes three functions, located in `src/extensions/wwpdb/struct-conn/index.ts`.
|
||||
|
||||
- `getStructConns` - to retrieve struct_conn records from a loaded structure
|
||||
- `inspectStructConn` - to visualize a struct_conn
|
||||
- `clearStructConnInspections` - to remove visulizations created by `inspectStructConn`
|
||||
|
||||
|
||||
## Example 1
|
||||
|
||||
The following example is a minimal HTML using this functionality:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer</title>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body style="margin: 0px;">
|
||||
<div style="position: absolute; width: 100%; height: 10%; padding-block: 10px;">
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf1');">disulf1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf2');">disulf2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale1');">covale1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale2');">covale2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale3');">covale3</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale4');">covale4</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc1');">metalc1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc2');">metalc2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc3');">metalc3</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc4');">metalc4</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, '5elb');">CLEAR</button>
|
||||
</div>
|
||||
<div id="app" style="position: absolute; top: 10%; width: 100%; height: 90%;"></div>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var molstarViewer;
|
||||
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
|
||||
molstarViewer = viewer;
|
||||
viewer.loadPdb('5elb');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The PDB ID (`'5elb'`) can be replaced be `undefined`, in which case the functions will apply to the first loaded structure.
|
||||
|
||||
|
||||
## Example 2
|
||||
|
||||
This is a more elaborated example, which automatically loads `5elb` (or any PDB entry given in the URL after `?pdb=`), retrieves the list of struct_conns, and creates a button for each struct_conn.
|
||||
|
||||
Be aware that some of the struct_conns may be present in the deposited model but not in the preferred assembly (default view). The presented example will raise a dialog window with error message in such cases, e.g. `disulf6` in entry `5elb`.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer - StructConn Extension Demo</title>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<style>
|
||||
body { margin: 0px; }
|
||||
#app { position: absolute; width: 85%; height: 100%; }
|
||||
#controls { position: absolute; right: 0; width: 15%; height: 100%; display: flex; flex-direction: column; overflow-y: scroll; }
|
||||
h1 { text-align: center; margin: 12px; font-weight: bold; font-size: 120%; }
|
||||
button { margin: 4px; margin-top: 0px; }
|
||||
</style>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="controls">
|
||||
<h1 id="pdb-id">Loading...</h1>
|
||||
<button onclick="clearInspections();">CLEAR</button>
|
||||
</div>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var pdbId = window.location.search.match(/[?&]pdb=(\w+)/i)?.[1]?.toLowerCase() ?? '5elb';
|
||||
var molstarViewer;
|
||||
function inspect(structConnId) {
|
||||
if (molstarViewer?.plugin) {
|
||||
molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, pdbId, structConnId).then(nSelectedAtoms => {
|
||||
if (nSelectedAtoms < 2) alert('Some of the interacting atoms were not found :(\n(maybe not present in the viewed assembly)');
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearInspections() {
|
||||
if (molstarViewer?.plugin) {
|
||||
molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, pdbId);
|
||||
}
|
||||
}
|
||||
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
|
||||
molstarViewer = viewer;
|
||||
return viewer.loadPdb(pdbId);
|
||||
}).then(() => {
|
||||
const structConns = molstar.PluginExtensions.wwPDBStructConn.getStructConns(molstarViewer.plugin, pdbId);
|
||||
const controls = document.getElementById('controls');
|
||||
for (const structConnId in structConns) {
|
||||
const button = document.createElement('button');
|
||||
button.innerText = structConnId;
|
||||
button.addEventListener('click', () => inspect(structConnId));
|
||||
controls.appendChild(button);
|
||||
};
|
||||
document.getElementById('pdb-id').innerHTML = pdbId;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -26,6 +26,7 @@
|
||||
* Non-standard residues
|
||||
* Protein (1BRR, 5Z6Y)
|
||||
* DNA (5D3G)
|
||||
* Collagen (6JEC)
|
||||
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
|
||||
* Long linear sugar chain (4HG6)
|
||||
* Anisotropic B-factors/Ellipsoids (1EJG)
|
||||
|
||||
75130
examples/7qpd.fw2.cif
Normal file
75130
examples/7qpd.fw2.cif
Normal file
File diff suppressed because it is too large
Load Diff
9005
package-lock.json
generated
9005
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
100
package.json
100
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "3.31.2",
|
||||
"version": "3.40.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -95,74 +95,78 @@
|
||||
"Gianluca Tomasello <giagitom@gmail.com>",
|
||||
"Ke Ma <mark.ma@rcsb.org>",
|
||||
"Jason Pattle <jpattle@exscientia.co.uk>",
|
||||
"David Williams <dwilliams@nobiastx.com>"
|
||||
"David Williams <dwilliams@nobiastx.com>",
|
||||
"Zhenyu Zhang <jump2cn@gmail.com>",
|
||||
"Russell Parker <russell@benchling.com>",
|
||||
"Dominik Tichy <tichydominik451@gmail.com>",
|
||||
"Yana Rose <yana.v.rose@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^4.0.0",
|
||||
"@graphql-codegen/cli": "^3.0.0",
|
||||
"@graphql-codegen/time": "^4.0.0",
|
||||
"@graphql-codegen/typescript": "^3.0.0",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/gl": "^6.0.2",
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/time": "^5.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^3.0.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^6.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@types/cors": "^2.8.14",
|
||||
"@types/gl": "^6.0.3",
|
||||
"@types/jpeg-js": "^0.3.7",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@types/pngjs": "^6.0.2",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/react": "^18.2.23",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^7.6.0",
|
||||
"cpx2": "^4.2.0",
|
||||
"concurrently": "^8.2.1",
|
||||
"cpx2": "^5.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.33.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.50.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"graphql": "^16.8.1",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.4.1",
|
||||
"mini-css-extract-plugin": "^2.7.2",
|
||||
"jest": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.58.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"simple-git": "^3.16.0",
|
||||
"sass": "^1.68.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"simple-git": "^3.20.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typescript": "^4.9.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.10",
|
||||
"@types/benchmark": "^2.1.2",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/swagger-ui-dist": "3.30.1",
|
||||
"@types/argparse": "^2.0.11",
|
||||
"@types/benchmark": "^2.1.3",
|
||||
"@types/compression": "1.7.3",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/node": "^16.18.55",
|
||||
"@types/node-fetch": "^2.6.6",
|
||||
"@types/swagger-ui-dist": "3.30.2",
|
||||
"argparse": "^2.0.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^9.0.19",
|
||||
"immutable": "^4.2.3",
|
||||
"node-fetch": "^2.6.9",
|
||||
"rxjs": "^7.8.0",
|
||||
"swagger-ui-dist": "^4.15.5",
|
||||
"tslib": "^2.5.0",
|
||||
"util.promisify": "^1.1.1",
|
||||
"immer": "^9.0.21",
|
||||
"immutable": "^4.3.4",
|
||||
"node-fetch": "^2.7.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.9.0",
|
||||
"tslib": "^2.6.2",
|
||||
"util.promisify": "^1.1.2",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -15,7 +15,7 @@ const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
|
||||
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
|
||||
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics -->`;
|
||||
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><iframe src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;"></iframe>`;
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -45,11 +45,13 @@ function occlusionStyle(plugin: PluginContext) {
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
samples: 32,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
} },
|
||||
outline: { name: 'on', params: {
|
||||
scale: 1.0,
|
||||
|
||||
@@ -48,6 +48,9 @@ import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
|
||||
@@ -56,7 +59,7 @@ const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
const Extensions = {
|
||||
export const ExtensionMap = {
|
||||
'volseg': PluginSpec.Behavior(Volseg),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'cellpack': PluginSpec.Behavior(CellPack),
|
||||
@@ -71,11 +74,14 @@ const Extensions = {
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
extensions: ObjectKeys(ExtensionMap),
|
||||
disabledExtensions: [] as string[],
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
@@ -126,11 +132,13 @@ export class Viewer {
|
||||
const o: ViewerOptions = { ...DefaultViewerOptions, ...definedOptions };
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const disabledExtension = new Set(o.disabledExtensions ?? []);
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
actions: defaultSpec.actions,
|
||||
behaviors: [
|
||||
...defaultSpec.behaviors,
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
@@ -503,8 +511,14 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
|
||||
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
};
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
|
||||
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
|
||||
|
||||
molstar.Viewer.create('app', {
|
||||
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
collapseLeftPanel: collapseLeftPanel,
|
||||
|
||||
@@ -81,5 +81,5 @@ export const DefaultDataOptions: DataOptions = {
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
|
||||
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
|
||||
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
const CCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
|
||||
@@ -22,6 +22,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'uline':
|
||||
case 'uchar3':
|
||||
case 'uchar1':
|
||||
case 'uchar5':
|
||||
// only force lower-case for enums
|
||||
return values && values.length ? EnumCol(values.map(x => x.toLowerCase()), 'lstr', description) : StrCol(description);
|
||||
case 'aliasname':
|
||||
@@ -61,6 +62,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'symop':
|
||||
case 'exp_data_doi':
|
||||
case 'asym_id':
|
||||
case 'uniprot_ptm_id':
|
||||
return StrCol(description);
|
||||
case 'int':
|
||||
case 'non_negative_int':
|
||||
@@ -89,6 +91,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'Tag':
|
||||
case 'Implied':
|
||||
case 'Word':
|
||||
case 'Uri':
|
||||
return wrapContainer('str', ',', description, container);
|
||||
case 'Real':
|
||||
return wrapContainer('float', ',', description, container);
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import gl from 'gl';
|
||||
import pngjs from 'pngjs';
|
||||
import jpegjs from 'jpeg-js';
|
||||
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
interface Args {
|
||||
pdbId: string,
|
||||
outDirectory: string
|
||||
@@ -42,7 +48,8 @@ async function main() {
|
||||
console.log('Outputs:', args.outDirectory);
|
||||
|
||||
// Create a headless plugin
|
||||
const plugin = new HeadlessPluginContext(DefaultPluginSpec(), { width: 800, height: 800 });
|
||||
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
|
||||
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
|
||||
await plugin.init();
|
||||
|
||||
// Download and visualize data in the plugin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -24,9 +24,31 @@ const Canvas3DPresets = {
|
||||
illustrative: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
|
||||
shadow: { name: 'off', params: {} },
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.33,
|
||||
color: Color(0x000000),
|
||||
includeTransparent: true,
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 1.0,
|
||||
@@ -37,9 +59,25 @@ const Canvas3DPresets = {
|
||||
occlusion: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'off', params: {} },
|
||||
shadow: { name: 'off', params: {} },
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
shadow: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
|
||||
@@ -50,7 +50,7 @@ const _testBasis: Basis = {
|
||||
0.025886090588624934,
|
||||
0.019164790004065606,
|
||||
-0.013539970104105408
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
@@ -101,7 +101,7 @@ const _testBasis: Basis = {
|
||||
0.5082729578468134,
|
||||
1.6880351220025265,
|
||||
0.4963443067810461
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
@@ -158,7 +158,7 @@ const _testBasis: Basis = {
|
||||
1.1367367844436005,
|
||||
-0.47018519422670163,
|
||||
-1.356802622574504
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function sphericalCollocation(
|
||||
L,
|
||||
shell.coefficients[amIndex++],
|
||||
shell.exponents,
|
||||
atom.center,
|
||||
atom.center as unknown as Vec3,
|
||||
cutoffThreshold,
|
||||
alpha
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SphericalElectronShell {
|
||||
export interface Basis {
|
||||
atoms: {
|
||||
// in Bohr units!
|
||||
center: Vec3;
|
||||
center: [number, number, number];
|
||||
shells: SphericalElectronShell[];
|
||||
}[];
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
|
||||
const count = geometry.length;
|
||||
const box = Box3D.expand(
|
||||
Box3D(),
|
||||
Box3D.fromVec3Array(Box3D(), geometry),
|
||||
Box3D.fromVec3Array(Box3D(), geometry as unknown as Vec3[]),
|
||||
Vec3.create(expand, expand, expand)
|
||||
);
|
||||
const size = Box3D.size(Vec3(), box);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -28,6 +28,12 @@ export const Backgrounds = PluginBehavior.create<{ }>({
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.config.set(PluginConfig.Background.Styles, [
|
||||
[{
|
||||
variant: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
}
|
||||
}, 'Off'],
|
||||
[{
|
||||
variant: {
|
||||
name: 'radialGradient',
|
||||
@@ -50,6 +56,7 @@ export const Backgrounds = PluginBehavior.create<{ }>({
|
||||
lightness: 0,
|
||||
saturation: 0,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
coverage: 'viewport',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -600,10 +600,12 @@ export const LoadCellPackModel = StateAction.build({
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 8,
|
||||
bias: 1,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
|
||||
@@ -31,7 +31,8 @@ type NtCTubeColors = typeof NtCTubeColors;
|
||||
export const NtCTubeColorThemeParams = {
|
||||
colors: PD.MappedStatic('default', {
|
||||
'default': PD.EmptyGroup(),
|
||||
'custom': PD.Group(getColorMapParams(NtCTubeColors))
|
||||
'custom': PD.Group(getColorMapParams(NtCTubeColors)),
|
||||
'uniform': PD.Color(Color(0xEEEEEE)),
|
||||
}),
|
||||
markResidueBoundaries: PD.Boolean(true),
|
||||
markSegmentBoundaries: PD.Boolean(true),
|
||||
@@ -43,7 +44,15 @@ export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
|
||||
}
|
||||
|
||||
export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
|
||||
const colorMap = props.colors.name === 'default' ? NtCTubeColors : props.colors.params;
|
||||
const colorMap = props.colors.name === 'default'
|
||||
? NtCTubeColors
|
||||
: props.colors.name === 'custom'
|
||||
? props.colors.params
|
||||
: ColorMap({
|
||||
...Object.fromEntries(ObjectKeys(NtCTubeColors).map(item => [item, props.colors.params])),
|
||||
residueMarker: NtCTubeColors.residueMarker,
|
||||
stepBoundaryMarker: NtCTubeColors.stepBoundaryMarker
|
||||
}) as NtCTubeColors;
|
||||
|
||||
function color(location: Location, isSecondary: boolean): Color {
|
||||
if (NTT.isLocation(location)) {
|
||||
|
||||
@@ -316,7 +316,7 @@ function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure,
|
||||
radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
|
||||
};
|
||||
mb.currentGroup = FirstBlockId;
|
||||
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, cylinderProps);
|
||||
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Un
|
||||
|
||||
function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
|
||||
const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
|
||||
if (eI !== -1)
|
||||
if (eI !== -1) {
|
||||
loc.unit.conformation.invariantPosition(eI, vec);
|
||||
else {
|
||||
vec[0] = 0; vec[1] = 0; vec[2] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Atom not found
|
||||
}
|
||||
|
||||
const p_1 = Vec3();
|
||||
@@ -29,19 +30,38 @@ const p3 = Vec3();
|
||||
const p4 = Vec3();
|
||||
const pP = Vec3();
|
||||
|
||||
const C5PrimeNames = ['C5\'', 'C5*'];
|
||||
const O3PrimeNames = ['O3\'', 'O3*'];
|
||||
const O5PrimeNames = ['O5\'', 'O5*'];
|
||||
const PNames = ['P'];
|
||||
|
||||
function getPoints(
|
||||
loc: StructureElement.Location,
|
||||
r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
|
||||
altId0: string, altId1: string, altId2: string,
|
||||
insCode0: string, insCode1: string, insCode2: string,
|
||||
) {
|
||||
if (r0) getAtomPosition(p_1, loc, r0, ['C5\'', 'C5*'], altId0, insCode0);
|
||||
r0 ? getAtomPosition(p0, loc, r0, ['O3\'', 'O3*'], altId0, insCode0) : getAtomPosition(p0, loc, r1, ['O5\'', 'O5*'], altId1, insCode1);
|
||||
getAtomPosition(p1, loc, r1, ['C5\'', 'C5*'], altId1, insCode1);
|
||||
getAtomPosition(p2, loc, r1, ['O3\'', 'O3*'], altId1, insCode1);
|
||||
getAtomPosition(p3, loc, r2, ['C5\'', 'C5*'], altId2, insCode2);
|
||||
getAtomPosition(p4, loc, r2, ['O3\'', 'O3*'], altId2, insCode2);
|
||||
getAtomPosition(pP, loc, r2, ['P'], altId2, insCode2);
|
||||
if (r0) {
|
||||
if (!getAtomPosition(p_1, loc, r0, C5PrimeNames, altId0, insCode0))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p0, loc, r0, O3PrimeNames, altId0, insCode0))
|
||||
return void 0;
|
||||
} else {
|
||||
if (!getAtomPosition(p0, loc, r1, O5PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
}
|
||||
|
||||
if (!getAtomPosition(p1, loc, r1, C5PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p2, loc, r1, O3PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
|
||||
if (!getAtomPosition(p3, loc, r2, C5PrimeNames, altId2, insCode2))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p4, loc, r2, O3PrimeNames, altId2, insCode2))
|
||||
return void 0;
|
||||
if (!getAtomPosition(pP, loc, r2, PNames, altId2, insCode2))
|
||||
return void 0;
|
||||
|
||||
return { p_1, p0, p1, p2, p3, p4, pP };
|
||||
}
|
||||
@@ -142,9 +162,12 @@ export class NtCTubeSegmentsIterator {
|
||||
const insCodeTwo = step.PDB_ins_code_2;
|
||||
const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
|
||||
const precedesDiscontinuity = r3 ? r3.index !== r2.index + 1 : false;
|
||||
const points = getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo);
|
||||
if (!points)
|
||||
return void 0;
|
||||
|
||||
return {
|
||||
...getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo),
|
||||
...points,
|
||||
stepIdx,
|
||||
followsGap,
|
||||
firstInChain: !r0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -13,7 +13,7 @@ import { PLUGIN_VERSION } from '../../mol-plugin/version';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { fillSerial } from '../../mol-util/array';
|
||||
import { NumberArray } from '../../mol-util/type-helpers';
|
||||
import { NumberArray, assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
@@ -35,6 +35,15 @@ const BIN_CHUNK_TYPE = 0x004E4942;
|
||||
const JSON_PAD_CHAR = 0x20;
|
||||
const BIN_PAD_CHAR = 0x00;
|
||||
|
||||
function getPrimitiveMode(mode: 'points' | 'lines' | 'triangles'): number {
|
||||
switch (mode) {
|
||||
case 'points': return 0;
|
||||
case 'lines': return 1;
|
||||
case 'triangles': return 4;
|
||||
default: assertUnreachable(mode);
|
||||
}
|
||||
}
|
||||
|
||||
export type GlbData = {
|
||||
glb: Uint8Array
|
||||
}
|
||||
@@ -89,12 +98,12 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return accessorOffset;
|
||||
}
|
||||
|
||||
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
|
||||
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array | undefined, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
|
||||
const tmpV = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const vertexArray = new Float32Array(vertexCount * 3);
|
||||
const normalArray = new Float32Array(vertexCount * 3);
|
||||
let normalArray: Float32Array | undefined;
|
||||
let indexArray: Uint32Array | undefined;
|
||||
|
||||
// position
|
||||
@@ -104,32 +113,35 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, normals, i * stride);
|
||||
v3normalize(tmpV, tmpV);
|
||||
v3toArray(tmpV, normalArray, i * 3);
|
||||
if (normals) {
|
||||
normalArray = new Float32Array(vertexCount * 3);
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, normals, i * stride);
|
||||
v3normalize(tmpV, tmpV);
|
||||
v3toArray(tmpV, normalArray, i * 3);
|
||||
}
|
||||
}
|
||||
|
||||
// face
|
||||
if (!isGeoTexture) {
|
||||
indexArray = indices!.slice(0, drawCount);
|
||||
if (!isGeoTexture && indices) {
|
||||
indexArray = indices.slice(0, drawCount);
|
||||
}
|
||||
|
||||
const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
|
||||
|
||||
let vertexBuffer = vertexArray.buffer;
|
||||
let normalBuffer = normalArray.buffer;
|
||||
let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
|
||||
let normalBuffer = normalArray?.buffer;
|
||||
let indexBuffer = (isGeoTexture || !indexArray) ? undefined : indexArray.buffer;
|
||||
if (!IsNativeEndianLittle) {
|
||||
vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
|
||||
normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
|
||||
if (normalBuffer) normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
|
||||
if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
|
||||
}
|
||||
|
||||
return {
|
||||
vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
|
||||
normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
|
||||
indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
|
||||
normalAccessorIndex: normalBuffer ? this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER) : undefined,
|
||||
indexAccessorIndex: (isGeoTexture || !indexBuffer) ? undefined : this.addBuffer(indexBuffer, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,8 +170,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
|
||||
}
|
||||
|
||||
private addMaterial(metalness: number, roughness: number) {
|
||||
const hash = `${metalness}|${roughness}`;
|
||||
private addMaterial(metalness: number, roughness: number, doubleSided: boolean, alpha: boolean) {
|
||||
const hash = `${metalness}|${roughness}|${doubleSided}`;
|
||||
if (!this.materialMap.has(hash)) {
|
||||
this.materialMap.set(hash, this.materials.length);
|
||||
this.materials.push({
|
||||
@@ -167,14 +179,16 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
baseColorFactor: [1, 1, 1, 1],
|
||||
metallicFactor: metalness,
|
||||
roughnessFactor: roughness
|
||||
}
|
||||
},
|
||||
doubleSided,
|
||||
alphaMode: alpha ? 'BLEND' : 'OPAQUE',
|
||||
});
|
||||
}
|
||||
return this.materialMap.get(hash)!;
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
|
||||
const t = Mat4();
|
||||
|
||||
@@ -186,32 +200,34 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
const metalness = values.uMetalness.ref.value;
|
||||
const roughness = values.uRoughness.ref.value;
|
||||
const doubleSided = values.uDoubleSided?.ref.value || values.hasReflection.ref.value;
|
||||
const alpha = values.uAlpha.ref.value < 1;
|
||||
|
||||
const material = this.addMaterial(metalness, roughness);
|
||||
const material = this.addMaterial(metalness, roughness, doubleSided, alpha);
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
interpolatedColors = GlbExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
// instancing
|
||||
const sameGeometryBuffers = mesh !== undefined;
|
||||
const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
|
||||
let vertexAccessorIndex: number;
|
||||
let normalAccessorIndex: number;
|
||||
let normalAccessorIndex: number | undefined;
|
||||
let indexAccessorIndex: number | undefined;
|
||||
let colorAccessorIndex: number;
|
||||
let meshIndex: number;
|
||||
@@ -235,7 +251,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a color buffer if needed
|
||||
if (instanceIndex === 0 || !sameColorBuffer) {
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
}
|
||||
|
||||
// glTF mesh
|
||||
@@ -248,7 +264,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
COLOR_0: colorAccessorIndex!
|
||||
},
|
||||
indices: indexAccessorIndex,
|
||||
material
|
||||
material,
|
||||
mode: getPrimitiveMode(mode),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -28,24 +28,32 @@ import { Color } from '../../mol-util/color/color';
|
||||
import { unpackRGBToInt } from '../../mol-util/number-packing';
|
||||
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
|
||||
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
|
||||
import { assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
|
||||
const GeoExportName = 'geo-export';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3sub = Vec3.sub;
|
||||
const v3dot = Vec3.dot;
|
||||
const v3unitY = Vec3.unitY;
|
||||
|
||||
type MeshMode = 'points' | 'lines' | 'triangles'
|
||||
|
||||
export interface AddMeshInput {
|
||||
mesh: {
|
||||
vertices: Float32Array
|
||||
normals: Float32Array
|
||||
normals: Float32Array | undefined
|
||||
indices: Uint32Array | undefined
|
||||
groups: Float32Array | Uint8Array
|
||||
vertexCount: number
|
||||
drawCount: number
|
||||
} | undefined
|
||||
meshes: Mesh[] | undefined
|
||||
values: BaseValues
|
||||
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
webgl: WebGLContext | undefined
|
||||
ctx: RuntimeContext
|
||||
}
|
||||
@@ -55,7 +63,8 @@ export type MeshGeoData = {
|
||||
groups: Float32Array | Uint8Array,
|
||||
vertexCount: number,
|
||||
instanceIndex: number,
|
||||
isGeoTexture: boolean
|
||||
isGeoTexture: boolean,
|
||||
mode: MeshMode
|
||||
}
|
||||
|
||||
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
|
||||
@@ -222,7 +231,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
|
||||
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
|
||||
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const uColor = values.uColor.ref.value;
|
||||
@@ -231,6 +240,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const dOverpaint = values.dOverpaint.ref.value;
|
||||
const tOverpaint = values.tOverpaint.ref.value.array;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
@@ -298,12 +313,18 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
|
||||
protected static getTransparency(vertexIndex: number, geoData: MeshGeoData, interpolatedTransparency?: Uint8Array): number {
|
||||
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const tTransparency = values.tTransparency.ref.value.array;
|
||||
const transparencyType = values.dTransparencyType.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
let transparency: number = 0;
|
||||
if (dTransparency) {
|
||||
switch (transparencyType) {
|
||||
@@ -329,7 +350,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
return transparency;
|
||||
}
|
||||
|
||||
protected abstract addMeshWithColors(input: AddMeshInput): void;
|
||||
protected abstract addMeshWithColors(input: AddMeshInput): Promise<void>;
|
||||
|
||||
private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
@@ -349,36 +370,132 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
drawCount = values.drawCount.ref.value;
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
|
||||
const drawCount = values.drawCount.ref.value / (2 * 3);
|
||||
|
||||
if (this.options.linesAsTriangles) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const radialSegments = 6;
|
||||
const topCap = true;
|
||||
const bottomCap = true;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0, il = vertexCount * 2; i < il; i += 4) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
} else {
|
||||
const n = vertexCount / 2;
|
||||
const vertices = new Float32Array(n * 2 * 3);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
vertices[i * 6] = aStart[i * 4 * 3];
|
||||
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
|
||||
|
||||
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
|
||||
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
if (this.options.pointsAsTriangles) {
|
||||
const center = Vec3();
|
||||
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const detail = 0;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
state.currentGroup = group;
|
||||
addSphere(state, center, radius, detail);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
} else {
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const center = Vec3();
|
||||
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const aPosition = values.centerBuffer.ref.value;
|
||||
const aGroup = values.groupBuffer.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const sphereCount = vertexCount / 4 * instanceCount;
|
||||
const sphereCount = vertexCount / 6 * instanceCount;
|
||||
let detail: number;
|
||||
if (sphereCount < 2000) detail = 3;
|
||||
else if (sphereCount < 20000) detail = 2;
|
||||
else detail = 1;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
if (sphereCount < 2000) detail = 3;
|
||||
else if (sphereCount < 20000) detail = 2;
|
||||
else detail = 1;
|
||||
break;
|
||||
case 'high':
|
||||
detail = 3;
|
||||
break;
|
||||
case 'medium':
|
||||
detail = 2;
|
||||
break;
|
||||
case 'low':
|
||||
detail = 1;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 4) {
|
||||
for (let i = 0; i < sphereCount; ++i) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
@@ -390,12 +507,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
const dir = Vec3();
|
||||
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
@@ -408,9 +526,24 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
|
||||
const cylinderCount = vertexCount / 6 * instanceCount;
|
||||
let radialSegments: number;
|
||||
if (cylinderCount < 2000) radialSegments = 36;
|
||||
else if (cylinderCount < 20000) radialSegments = 24;
|
||||
else radialSegments = 12;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
if (cylinderCount < 2000) radialSegments = 36;
|
||||
else if (cylinderCount < 20000) radialSegments = 24;
|
||||
else radialSegments = 12;
|
||||
break;
|
||||
case 'high':
|
||||
radialSegments = 36;
|
||||
break;
|
||||
case 'medium':
|
||||
radialSegments = 24;
|
||||
break;
|
||||
case 'low':
|
||||
radialSegments = 12;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
@@ -418,13 +551,17 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
for (let i = 0; i < vertexCount; i += 6) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
v3sub(dir, end, start);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
const topCap = cap === 1 || cap === 3;
|
||||
const bottomCap = cap >= 2;
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
let topCap = cap === 1 || cap === 3;
|
||||
let bottomCap = cap >= 2;
|
||||
if (v3dot(v3unitY, dir) > 0) {
|
||||
[bottomCap, topCap] = [topCap, bottomCap];
|
||||
}
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
@@ -432,7 +569,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
@@ -457,11 +594,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (!renderObject.state.visible) return;
|
||||
if (!renderObject.state.visible && !this.options.includeHidden) return;
|
||||
if (renderObject.values.drawCount.ref.value === 0) return;
|
||||
if (renderObject.values.instanceCount.ref.value === 0) return;
|
||||
|
||||
switch (renderObject.type) {
|
||||
case 'mesh':
|
||||
@@ -479,6 +618,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
}
|
||||
|
||||
protected options = {
|
||||
includeHidden: false,
|
||||
linesAsTriangles: false,
|
||||
pointsAsTriangles: false,
|
||||
primitivesQuality: 'auto' as 'auto' | 'high' | 'medium' | 'low',
|
||||
};
|
||||
|
||||
abstract getData(ctx: RuntimeContext): Promise<D>;
|
||||
|
||||
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -70,7 +70,8 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const obj = this.obj;
|
||||
const t = Mat4();
|
||||
@@ -86,19 +87,19 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = ObjExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
interpolatedColors = ObjExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
@@ -126,7 +127,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
|
||||
StringBuilder.writeSafe(obj, 'vn ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
@@ -136,7 +137,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
|
||||
// color
|
||||
const quantizedColors = new Uint8Array(drawCount * 3);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
@@ -30,7 +30,8 @@ export class StlExporter extends MeshExporter<StlData> {
|
||||
private centerTransform: Mat4;
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { values, isGeoTexture, ctx } = input;
|
||||
const { values, isGeoTexture, mode, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const t = Mat4();
|
||||
const tmpV = Vec3();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -61,7 +61,8 @@ def Material "material${materialKey}"
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
@@ -78,20 +79,20 @@ def Material "material${materialKey}"
|
||||
const roughness = values.uRoughness.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
@@ -123,7 +124,7 @@ def Material "material${materialKey}"
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
|
||||
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
|
||||
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
|
||||
StringBuilder.writeSafe(normalBuilder, ',');
|
||||
@@ -133,7 +134,7 @@ def Material "material${materialKey}"
|
||||
StringBuilder.writeSafe(normalBuilder, ')');
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; ++i) {
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
|
||||
import { to_mmCIF, Unit } from '../../mol-model/structure';
|
||||
import { Structure, to_mmCIF, Unit } from '../../mol-model/structure';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { getFormattedTime } from '../../mol-util/date';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
|
||||
const ModelExportNameProp = '__ModelExportName__';
|
||||
export const ModelExport = {
|
||||
getStructureName(structure: Structure): string | undefined {
|
||||
return structure.inheritedPropertyData[ModelExportNameProp];
|
||||
},
|
||||
setStructureName(structure: Structure, name: string) {
|
||||
return structure.inheritedPropertyData[ModelExportNameProp] = name;
|
||||
}
|
||||
};
|
||||
|
||||
export async function exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
|
||||
try {
|
||||
await plugin.runTask(_exportHierarchy(plugin, options), { useOverlay: true });
|
||||
@@ -43,19 +54,21 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = entryMap.has(s.model.entryId)
|
||||
? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
|
||||
: `${s.model.entryId}.${format}`;
|
||||
entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
|
||||
const name = ModelExport.getStructureName(s) || s.model.entryId || 'unnamed';
|
||||
|
||||
await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
|
||||
const fileName = entryMap.has(name)
|
||||
? `${name}_${entryMap.get(name)! + 1}.${format}`
|
||||
: `${name}.${format}`;
|
||||
entryMap.set(name, (entryMap.get(name) ?? 0) + 1);
|
||||
|
||||
await ctx.update({ message: `Exporting ${name}...`, isIndeterminate: true, canAbort: false });
|
||||
if (s.elementCount > 100000) {
|
||||
// Give UI chance to update, only needed for larger structures.
|
||||
await new Promise(res => setTimeout(res, 50));
|
||||
}
|
||||
|
||||
try {
|
||||
files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
|
||||
files.push([fileName, to_mmCIF(name, s, format === 'bcif', { copyAllCategories: true })]);
|
||||
} catch (e) {
|
||||
if (format === 'cif' && s.elementCount > 2000000) {
|
||||
plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@ namespace ValidationReport {
|
||||
Clashes = 'rcsb-clashes',
|
||||
}
|
||||
|
||||
export const DefaultBaseUrl = '//ftp.rcsb.org/pub/pdb/validation_reports';
|
||||
export const DefaultBaseUrl = 'https://files.rcsb.org/pub/pdb/validation_reports';
|
||||
export function getEntryUrl(pdbId: string, baseUrl: string) {
|
||||
const id = pdbId.toLowerCase();
|
||||
return `${baseUrl}/${id.substr(1, 2)}/${id}/${id}_validation.xml.gz`;
|
||||
|
||||
30
src/extensions/sb-ncbr/README.md
Normal file
30
src/extensions/sb-ncbr/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# SB NCBR extensions
|
||||
|
||||
## Partial charges
|
||||
|
||||
This extension visualizes partial atomic charges for atoms and residues. The extension reads charge data of a structure from a mmcif file and displays them as a color gradient on the atoms/residues. The coloring uses two gradients: one for positive charges (white-to-blue) and one for negative charges (red-to-white). The color is interpolated between the appropriate gradient based on the charge value. The extension also displays the charge values in the description label when an atom/residue is selected.
|
||||
|
||||
### How to use
|
||||
|
||||
To visualize partial charges, you need to provide a mmcif file with the structure and its charges. The charges are stored under the following categories:
|
||||
|
||||
```
|
||||
_sb_ncbr_partial_atomic_charges_meta.id # id of the charges (e.g. 1)
|
||||
_sb_ncbr_partial_atomic_charges_meta.type # type of the charges (optional, e.g. 'empirical')
|
||||
_sb_ncbr_partial_atomic_charges_meta.method # calculation method name (e.g. 'QEq', 'SQE+qp/Schindler 2021 (PUB_pept)')
|
||||
|
||||
_sb_ncbr_partial_atomic_charges.type_id # id of the charges (pointer to _sb_ncbr_partial_atomic_charges_meta.id)
|
||||
_sb_ncbr_partial_atomic_charges.atom_id # atom id (pointer to _atom_site.id)
|
||||
_sb_ncbr_partial_atomic_charges.charge # partial atomic charge
|
||||
```
|
||||
> Note that the mmcif item `_partial_atomic_charges_meta.method` is used as a description of the charge set in the UI (described in *Controls*).
|
||||
|
||||
The extension will automatically read the charges from the mmcif file and color the structure accordingly.
|
||||
|
||||
### Controls
|
||||
|
||||
The extension provides controls for setting the color gradient range and for selecting charge type (atom charges or residue charges).
|
||||
These controls are available in Color Theme settings for 3D Representation cells in the State Tree UI.
|
||||
|
||||
There is also a dropdown menu for switching between charge sets.
|
||||
These controls are available in Custom Model Properties settings for Model cell in the State Tree UI.
|
||||
3
src/extensions/sb-ncbr/index.ts
Normal file
3
src/extensions/sb-ncbr/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SbNcbrPartialCharges } from './partial-charges/behavior';
|
||||
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
38
src/extensions/sb-ncbr/partial-charges/behavior.ts
Normal file
38
src/extensions/sb-ncbr/partial-charges/behavior.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { SbNcbrPartialChargesColorThemeProvider } from './color';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { SbNcbrPartialChargesLociLabelProvider } from './labels';
|
||||
import { SbNcbrPartialChargesPreset } from './preset';
|
||||
|
||||
export const SbNcbrPartialCharges = PluginBehavior.create<{ autoAttach: boolean; showToolTip: boolean }>({
|
||||
name: 'sb-ncbr-partial-charges',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'SB NCBR Partial Charges',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean; showToolTip: boolean }> {
|
||||
private SbNcbrPartialChargesLociLabelProvider: LociLabelProvider = SbNcbrPartialChargesLociLabelProvider(
|
||||
this.ctx
|
||||
);
|
||||
|
||||
register(): void {
|
||||
this.ctx.customModelProperties.register(SbNcbrPartialChargesPropertyProvider, this.params.autoAttach);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.add(SbNcbrPartialChargesColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.addProvider(this.SbNcbrPartialChargesLociLabelProvider);
|
||||
this.ctx.builders.structure.representation.registerPreset(SbNcbrPartialChargesPreset);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customModelProperties.unregister(SbNcbrPartialChargesPropertyProvider.descriptor.name);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.remove(SbNcbrPartialChargesColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.removeProvider(this.SbNcbrPartialChargesLociLabelProvider);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(SbNcbrPartialChargesPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
showToolTip: PD.Boolean(true),
|
||||
}),
|
||||
});
|
||||
150
src/extensions/sb-ncbr/partial-charges/color.ts
Normal file
150
src/extensions/sb-ncbr/partial-charges/color.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Bond, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
|
||||
const Colors = {
|
||||
Bond: Color(0xffffff),
|
||||
Error: Color(0x00ff00),
|
||||
MissingCharge: Color(0xffffff),
|
||||
|
||||
Negative: Color(0xff0000),
|
||||
Zero: Color(0xffffff),
|
||||
Positive: Color(0x0000ff),
|
||||
|
||||
getColor: (charge: number, maxCharge: number): Color => {
|
||||
if (charge === 0) return Colors.Zero;
|
||||
if (charge <= -maxCharge) return Colors.Negative;
|
||||
if (charge >= maxCharge) return Colors.Positive;
|
||||
|
||||
const t = maxCharge !== 0 ? Math.abs(charge) / maxCharge : 1;
|
||||
const endColor = charge < 0 ? Colors.Negative : Colors.Positive;
|
||||
|
||||
return Color.interpolate(Colors.Zero, endColor, t);
|
||||
},
|
||||
};
|
||||
|
||||
export const PartialChargesThemeParams = {
|
||||
maxAbsoluteCharge: PD.Numeric(
|
||||
0,
|
||||
{ min: 0 },
|
||||
{
|
||||
label: 'Charge Range',
|
||||
}
|
||||
),
|
||||
absolute: PD.Boolean(false, { isHidden: false, label: 'Use Range' }),
|
||||
chargeType: PD.Select(
|
||||
'residue',
|
||||
[
|
||||
['atom', 'Atom charges'],
|
||||
['residue', 'Residue charges'],
|
||||
],
|
||||
{ isHidden: false }
|
||||
),
|
||||
};
|
||||
export type PartialChargesThemeParams = typeof PartialChargesThemeParams;
|
||||
|
||||
export function getPartialChargesThemeParams() {
|
||||
return PD.clone(PartialChargesThemeParams);
|
||||
}
|
||||
|
||||
export function PartialChargesColorTheme(
|
||||
ctx: ThemeDataContext,
|
||||
props: PD.Values<PartialChargesThemeParams>
|
||||
): ColorTheme<PartialChargesThemeParams> {
|
||||
const model = ctx.structure?.models[0];
|
||||
if (!model) {
|
||||
throw new Error('No model found');
|
||||
}
|
||||
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
|
||||
if (!data) {
|
||||
throw new Error('No partial charges data found');
|
||||
}
|
||||
|
||||
const { absolute, chargeType } = props;
|
||||
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge, maxAbsoluteAtomCharges, maxAbsoluteResidueCharges } = data;
|
||||
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
|
||||
const atomToCharge = typeIdToAtomIdToCharge.get(typeId);
|
||||
const residueToCharge = typeIdToResidueToCharge.get(typeId);
|
||||
|
||||
let maxCharge = 0;
|
||||
if (absolute) {
|
||||
maxCharge = props.maxAbsoluteCharge < 0 ? 0 : props.maxAbsoluteCharge;
|
||||
} else if (chargeType === 'atom') {
|
||||
maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
|
||||
} else {
|
||||
maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
|
||||
}
|
||||
|
||||
// forces coloring updates
|
||||
const contextHash = SbNcbrPartialChargesPropertyProvider.get(model)?.version;
|
||||
|
||||
const chargeMap = chargeType === 'atom' ? atomToCharge : residueToCharge;
|
||||
|
||||
let color: LocationColor;
|
||||
|
||||
if (!chargeMap) {
|
||||
color = (_: Location): Color => Colors.MissingCharge;
|
||||
} else {
|
||||
color = (location: Location): Color => {
|
||||
let id = -1;
|
||||
if (StructureElement.Location.is(location)) {
|
||||
if (Unit.isAtomic(location.unit)) {
|
||||
id = StructureProperties.atom.id(location);
|
||||
}
|
||||
} else if (Bond.isLocation(location)) {
|
||||
if (Unit.isAtomic(location.aUnit)) {
|
||||
const l = StructureElement.Location.create(ctx.structure?.root);
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
id = StructureProperties.atom.id(l);
|
||||
}
|
||||
}
|
||||
|
||||
const charge = chargeMap.get(id);
|
||||
|
||||
if (charge === undefined) {
|
||||
console.warn('No charge found for id', id);
|
||||
return Colors.MissingCharge;
|
||||
}
|
||||
|
||||
return Colors.getColor(charge, maxCharge);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
factory: PartialChargesColorTheme,
|
||||
granularity: 'group',
|
||||
color,
|
||||
props,
|
||||
description: 'Color atoms and residues based on their partial charge.',
|
||||
preferSmoothing: false,
|
||||
contextHash,
|
||||
};
|
||||
}
|
||||
|
||||
export const SbNcbrPartialChargesColorThemeProvider: ColorTheme.Provider<
|
||||
PartialChargesThemeParams,
|
||||
'sb-ncbr-partial-charges'
|
||||
> = {
|
||||
label: 'SB NCBR Partial Charges',
|
||||
name: 'sb-ncbr-partial-charges',
|
||||
category: ColorTheme.Category.Atom,
|
||||
factory: PartialChargesColorTheme,
|
||||
getParams: getPartialChargesThemeParams,
|
||||
defaultValues: PD.getDefaultValues(PartialChargesThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) =>
|
||||
!!ctx.structure &&
|
||||
ctx.structure.models.some((model) => SbNcbrPartialChargesPropertyProvider.isApplicable(model)),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) =>
|
||||
data.structure
|
||||
? SbNcbrPartialChargesPropertyProvider.attach(ctx, data.structure.models[0], void 0, true)
|
||||
: Promise.resolve(),
|
||||
detach: (data) => data.structure && SbNcbrPartialChargesPropertyProvider.ref(data.structure.models[0], false),
|
||||
},
|
||||
};
|
||||
40
src/extensions/sb-ncbr/partial-charges/labels.ts
Normal file
40
src/extensions/sb-ncbr/partial-charges/labels.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { StructureElement, StructureProperties } from '../../../mol-model/structure';
|
||||
import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { SbNcbrPartialChargesPropertyProvider, hasPartialChargesCategories } from './property';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
|
||||
export function SbNcbrPartialChargesLociLabelProvider(ctx: PluginContext): LociLabelProvider {
|
||||
return {
|
||||
label: (loci: Loci) => {
|
||||
if (!StructureElement.Loci.is(loci)) return;
|
||||
|
||||
const model = loci.structure.model;
|
||||
if (!hasPartialChargesCategories(model)) return;
|
||||
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
|
||||
if (!data) return;
|
||||
|
||||
const loc = StructureElement.Loci.getFirstLocation(loci);
|
||||
if (!loc) return;
|
||||
|
||||
const granularity = ctx.managers.interactivity.props.granularity;
|
||||
if (granularity !== 'element' && granularity !== 'residue') {
|
||||
return;
|
||||
}
|
||||
|
||||
const atomId = StructureProperties.atom.id(loc);
|
||||
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge } = data;
|
||||
|
||||
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
|
||||
const showResidueCharge = granularity === 'residue';
|
||||
const charge = showResidueCharge
|
||||
? typeIdToResidueToCharge.get(typeId)?.get(atomId)
|
||||
: typeIdToAtomIdToCharge.get(typeId)?.get(atomId);
|
||||
const label = granularity === 'residue' ? 'Residue charge' : 'Atom charge';
|
||||
|
||||
return `<strong>${label}: ${charge?.toFixed(4) || 'undefined'}</strong>`;
|
||||
},
|
||||
group: (label: LociLabel): string => (label as string).toString().replace(/Model [0-9]+/g, 'Models'),
|
||||
};
|
||||
}
|
||||
32
src/extensions/sb-ncbr/partial-charges/preset.ts
Normal file
32
src/extensions/sb-ncbr/partial-charges/preset.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
PresetStructureRepresentations,
|
||||
StructureRepresentationPresetProvider,
|
||||
} from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { SbNcbrPartialChargesColorThemeProvider } from './color';
|
||||
|
||||
export const SbNcbrPartialChargesPreset = StructureRepresentationPresetProvider({
|
||||
id: 'sb-ncbr-partial-charges-preset',
|
||||
display: {
|
||||
name: 'SB NCBR Partial Charges',
|
||||
group: 'Annotation',
|
||||
description: 'Color atoms and residues based on their partial charge.',
|
||||
},
|
||||
isApplicable(a) {
|
||||
return !!a.data.models.some((m) => SbNcbrPartialChargesPropertyProvider.isApplicable(m));
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const colorTheme = SbNcbrPartialChargesColorThemeProvider.name as any;
|
||||
return PresetStructureRepresentations.auto.apply(
|
||||
ref,
|
||||
{ ...params, theme: { globalName: colorTheme, focus: { name: colorTheme, params: { chargeType: 'atom' } } } },
|
||||
plugin
|
||||
);
|
||||
},
|
||||
});
|
||||
204
src/extensions/sb-ncbr/partial-charges/property.ts
Normal file
204
src/extensions/sb-ncbr/partial-charges/property.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { arrayMinMax } from '../../../mol-util/array';
|
||||
|
||||
type TypeId = number;
|
||||
type IdToCharge = Map<number, number>;
|
||||
export interface SBNcbrPartialChargeData {
|
||||
typeIdToMethod: Map<TypeId, string>;
|
||||
typeIdToAtomIdToCharge: Map<TypeId, IdToCharge>;
|
||||
typeIdToResidueToCharge: Map<TypeId, IdToCharge>;
|
||||
maxAbsoluteAtomCharges: IdToCharge;
|
||||
maxAbsoluteResidueCharges: IdToCharge;
|
||||
maxAbsoluteAtomChargeAll: number;
|
||||
params: PartialChargesPropertyParams;
|
||||
}
|
||||
|
||||
const PartialChargesPropertyParams = {
|
||||
typeId: PD.Select<number>(0, [[0, '0']]),
|
||||
};
|
||||
type PartialChargesPropertyParams = typeof PartialChargesPropertyParams;
|
||||
const DefaultPartialChargesPropertyParams = PD.clone(PartialChargesPropertyParams);
|
||||
|
||||
function getParams(model: Model) {
|
||||
return getData(model).value?.params ?? DefaultPartialChargesPropertyParams;
|
||||
}
|
||||
|
||||
const PropertyKey = 'sb-ncbr-partial-charges-property-data';
|
||||
|
||||
function getData(model: Model): CustomProperty.Data<SBNcbrPartialChargeData | undefined> {
|
||||
if (PropertyKey in model._staticPropertyData) {
|
||||
return model._staticPropertyData[PropertyKey];
|
||||
}
|
||||
|
||||
let data: CustomProperty.Data<SBNcbrPartialChargeData | undefined>;
|
||||
|
||||
if (!SbNcbrPartialChargesPropertyProvider.isApplicable(model)) {
|
||||
data = { value: undefined };
|
||||
} else {
|
||||
const typeIdToMethod = getTypeIdToMethod(model);
|
||||
const typeIdToAtomIdToCharge = getTypeIdToAtomIdToCharge(model);
|
||||
const typeIdToResidueToCharge = getTypeIdToResidueIdToCharge(model, typeIdToAtomIdToCharge);
|
||||
const maxAbsoluteAtomCharges = getMaxAbsoluteCharges(typeIdToAtomIdToCharge);
|
||||
const maxAbsoluteResidueCharges = getMaxAbsoluteCharges(typeIdToResidueToCharge);
|
||||
const maxAbsoluteAtomChargeAll = getMaxAbsoluteAtomChargeAll(maxAbsoluteAtomCharges, maxAbsoluteResidueCharges);
|
||||
|
||||
const options = Array.from(typeIdToMethod.entries()).map(
|
||||
([typeId, method]) => [typeId, method] as [number, string]
|
||||
);
|
||||
const params = {
|
||||
typeId: PD.Select<number>(1, options),
|
||||
};
|
||||
|
||||
data = {
|
||||
value: {
|
||||
typeIdToMethod,
|
||||
typeIdToAtomIdToCharge,
|
||||
typeIdToResidueToCharge,
|
||||
maxAbsoluteAtomCharges,
|
||||
maxAbsoluteResidueCharges,
|
||||
maxAbsoluteAtomChargeAll,
|
||||
params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
model._staticPropertyData[PropertyKey] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
function getTypeIdToMethod(model: Model) {
|
||||
const typeIdToMethod: SBNcbrPartialChargeData['typeIdToMethod'] = new Map();
|
||||
|
||||
const sourceData = model.sourceData as MmcifFormat;
|
||||
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.rowCount;
|
||||
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('id');
|
||||
const methods = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('method');
|
||||
|
||||
if (!typeIds || !methods) {
|
||||
return typeIdToMethod;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const typeId = typeIds.int(i);
|
||||
const method = methods.str(i);
|
||||
typeIdToMethod.set(typeId, method);
|
||||
}
|
||||
|
||||
return typeIdToMethod;
|
||||
}
|
||||
|
||||
function getTypeIdToAtomIdToCharge(model: Model): SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] {
|
||||
const atomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] = new Map();
|
||||
|
||||
const sourceData = model.sourceData as MmcifFormat;
|
||||
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.rowCount;
|
||||
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('type_id');
|
||||
const atomIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('atom_id');
|
||||
const charges = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('charge');
|
||||
|
||||
if (!typeIds || !atomIds || !charges) return atomIdToCharge;
|
||||
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const typeId = typeIds.int(i);
|
||||
const atomId = atomIds.int(i);
|
||||
const charge = charges.float(i);
|
||||
if (!atomIdToCharge.has(typeId)) atomIdToCharge.set(typeId, new Map());
|
||||
atomIdToCharge.get(typeId)?.set(atomId, charge);
|
||||
}
|
||||
|
||||
return atomIdToCharge;
|
||||
}
|
||||
|
||||
function getTypeIdToResidueIdToCharge(
|
||||
model: Model,
|
||||
typeIdToAtomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
|
||||
) {
|
||||
const { offsets, count } = model.atomicHierarchy.residueAtomSegments;
|
||||
const { atomId: atomIds } = model.atomicConformation;
|
||||
|
||||
const residueToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge'] = new Map();
|
||||
|
||||
typeIdToAtomIdToCharge.forEach((atomIdToCharge, typeId: number) => {
|
||||
if (!residueToCharge.has(typeId)) residueToCharge.set(typeId, new Map());
|
||||
const residueCharges = residueToCharge.get(typeId)!;
|
||||
for (let rI = 0; rI < count; rI++) {
|
||||
let charge = 0;
|
||||
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
|
||||
const atom_id = atomIds.value(aI);
|
||||
charge += atomIdToCharge.get(atom_id) || 0;
|
||||
}
|
||||
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
|
||||
const atom_id = atomIds.value(aI);
|
||||
residueCharges.set(atom_id, charge);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return residueToCharge;
|
||||
}
|
||||
|
||||
function getMaxAbsoluteCharges(
|
||||
typeIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
|
||||
): SBNcbrPartialChargeData['maxAbsoluteAtomCharges'];
|
||||
function getMaxAbsoluteCharges(
|
||||
typeIdToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge']
|
||||
): SBNcbrPartialChargeData['maxAbsoluteResidueCharges'] {
|
||||
const maxAbsoluteCharges: Map<number, number> = new Map();
|
||||
|
||||
typeIdToCharge.forEach((idToCharge, typeId) => {
|
||||
const charges = Array.from(idToCharge.values());
|
||||
const [min, max] = arrayMinMax(charges);
|
||||
const bound = Math.max(Math.abs(min), max);
|
||||
maxAbsoluteCharges.set(typeId, bound);
|
||||
});
|
||||
|
||||
return maxAbsoluteCharges;
|
||||
}
|
||||
|
||||
function getMaxAbsoluteAtomChargeAll(
|
||||
maxAbsoluteAtomCharges: SBNcbrPartialChargeData['maxAbsoluteAtomCharges'],
|
||||
maxAbsoluteResidueCharges: SBNcbrPartialChargeData['maxAbsoluteResidueCharges']
|
||||
): number {
|
||||
let maxAbsoluteCharge = 0;
|
||||
|
||||
maxAbsoluteAtomCharges.forEach((_, typeId) => {
|
||||
const maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
|
||||
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
|
||||
});
|
||||
maxAbsoluteResidueCharges.forEach((_, typeId) => {
|
||||
const maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
|
||||
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
|
||||
});
|
||||
|
||||
return maxAbsoluteCharge;
|
||||
}
|
||||
|
||||
export function hasPartialChargesCategories(model: Model): boolean {
|
||||
if (!model || !MmcifFormat.is(model.sourceData)) return false;
|
||||
const { categories } = model.sourceData.data.frame;
|
||||
return (
|
||||
'atom_site' in categories &&
|
||||
'sb_ncbr_partial_atomic_charges' in categories &&
|
||||
'sb_ncbr_partial_atomic_charges_meta' in categories
|
||||
);
|
||||
}
|
||||
|
||||
export const SbNcbrPartialChargesPropertyProvider: CustomModelProperty.Provider<
|
||||
PartialChargesPropertyParams,
|
||||
SBNcbrPartialChargeData | undefined
|
||||
> = CustomModelProperty.createProvider({
|
||||
label: 'SB NCBR Partial Charges Property Provider',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'sb-ncbr-partial-charges-property-provider',
|
||||
}),
|
||||
type: 'static',
|
||||
defaultParams: DefaultPartialChargesPropertyParams,
|
||||
getParams: (data: Model) => getParams(data),
|
||||
isApplicable: (model: Model) => hasPartialChargesCategories(model),
|
||||
obtain: (_ctx: CustomProperty.Context, model: Model) => Promise.resolve(getData(model)),
|
||||
});
|
||||
@@ -19,7 +19,7 @@ import { VolsegEntryFromRoot, VolsegGlobalStateFromRoot, VolsegStateFromEntry }
|
||||
import { VolsegUI } from './ui';
|
||||
|
||||
|
||||
const DEBUGGING = window.location.hostname === 'localhost';
|
||||
const DEBUGGING = typeof window !== 'undefined' ? window?.location?.hostname === 'localhost' : false;
|
||||
|
||||
export const VolsegVolumeServerConfig = {
|
||||
// DefaultServer: new PluginConfigItem('volseg-volume-server', DEFAULT_VOLUME_SERVER_V2),
|
||||
|
||||
47
src/extensions/wwpdb/ccd/README.md
Normal file
47
src/extensions/wwpdb/ccd/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Chemical Component Dictionary Extension
|
||||
|
||||
The [Chemical Component Dictionary (CCD)](https://www.wwpdb.org/data/ccd) describes all small molecules and monomers found in PDB entries. The dictionary provides a plethora of additional information not present in wwPDB archive structures such as chemical descriptors (SMILES & InChI) and stereochemical assignments, information on bond order and more. Most notably, the CCD provides 2 sets of coordinates:
|
||||
- `ideal`: idealized/minimized coordinates, obtained using Molecular Networks' Corina, and if there are issues, OpenEye's OMEGA
|
||||
- `model`: coordinates extracted from an archive structure
|
||||
|
||||
## How to Load a Component from URL
|
||||
1. "Download Structure" -- switch "Source" to "URL"
|
||||
2. Enter URL of component, e.g. https://files.rcsb.org/ligands/view/HEM.cif, leave "Format" as is
|
||||
3. Click "Apply"
|
||||
|
||||
This parses the corresponding component into 2 models (1st: `ideal` coordinates, 2nd: `model` coordinates) and applies the default representaiton to the 1st model. `model` coordinates are available as 2nd model. Click the canvas to re-focus if you don't see anything after switching models due to the coordinates being far away.
|
||||
|
||||
## How to Visualize Components
|
||||
There's a dedicated representation preset that faciliates the comparison of `ideal` and `model` coordinates.
|
||||
|
||||
1. Load a component as described above
|
||||
2. Switch structure preset to "Chemical Component" (button in the top-right, in the "Structure" panel)
|
||||
|
||||
This creates a dedicated component for `ideal` as well as `model` coordinates and represents them as ball-and-stick. Initially, only `ideal` coordinates are shown. After toggling the visibility of `model` coordinates, they appear superimposed with the `ideal` coordinates.
|
||||
|
||||
## Examples & Test Cases
|
||||
Ligand | Description | Details
|
||||
-- | -- | --
|
||||
https://files.rcsb.org/ligands/view/HEM.cif | metal coordination |
|
||||
https://files.rcsb.org/ligands/view/FE.cif | +3 oxidation state |
|
||||
https://files.rcsb.org/ligands/view/FE2.cif | +2 oxidation state |
|
||||
https://files.rcsb.org/ligands/view/RUC.cif | transition metal |
|
||||
https://files.rcsb.org/ligands/view/SF4.cif | Fe-S cluster | doesn't align nicely
|
||||
https://files.rcsb.org/ligands/view/TBR.cif | coords identical |
|
||||
https://files.rcsb.org/ligands/view/OER.cif | coords identical |
|
||||
https://files.rcsb.org/ligands/view/FEA.cif | charges |
|
||||
https://files.rcsb.org/ligands/view/PR2.cif | orientation differs |
|
||||
https://files.rcsb.org/ligands/view/03R.cif | some atoms missing |
|
||||
https://files.rcsb.org/ligands/view/02U.cif | many atoms missing |
|
||||
https://files.rcsb.org/ligands/view/HC0.cif | no ideal coords | unrelated: O and H atoms clashing
|
||||
https://files.rcsb.org/ligands/view/Q6O.cif | no model coords |
|
||||
https://files.rcsb.org/ligands/view/H0C.cif | big ligand |
|
||||
https://files.rcsb.org/ligands/view/2NC.cif | dual representation as PRD and CC |
|
||||
https://files.rcsb.org/birds/view/PRDCC_000001.cif | PRDCC |
|
||||
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/BB87Q.cif | extended CCD identifier |
|
||||
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/7ZTVU.cif | extended CCD identifier |
|
||||
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9QRZS.cif | extended CCD identifier |
|
||||
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9ABCD.cif | extended CCD identifier |
|
||||
https://files.rcsb.org/ligands/view/UNK.cif | CCD special: unknown amino acid | unrelated: some model H are placed far away
|
||||
https://files.rcsb.org/ligands/view/UNX.cif | CCD special: unknown atom/ion | no ideal coordinates
|
||||
https://files.rcsb.org/ligands/view/UNL.cif | CCD special: unknown ligand | no coordinates whatsoever
|
||||
33
src/extensions/wwpdb/ccd/behavior.ts
Normal file
33
src/extensions/wwpdb/ccd/behavior.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
|
||||
import { ChemicalComponentPreset, ChemicalCompontentTrajectoryHierarchyPreset } from './representation';
|
||||
|
||||
export const wwPDBChemicalComponentDictionary = PluginBehavior.create<{ }>({
|
||||
name: 'wwpdb-chemical-component-dictionary',
|
||||
category: 'representation',
|
||||
display: {
|
||||
name: 'wwPDB Chemical Compontent Dictionary',
|
||||
description: 'Custom representation for data loaded from the CCD.'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.builders.structure.hierarchy.registerPreset(ChemicalCompontentTrajectoryHierarchyPreset);
|
||||
this.ctx.builders.structure.representation.registerPreset(ChemicalComponentPreset);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.builders.structure.hierarchy.unregisterPreset(ChemicalCompontentTrajectoryHierarchyPreset);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(ChemicalComponentPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({ })
|
||||
});
|
||||
167
src/extensions/wwpdb/ccd/representation.ts
Normal file
167
src/extensions/wwpdb/ccd/representation.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { StateObjectRef, StateTransform } from '../../../mol-state';
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { Structure } from '../../../mol-model/structure';
|
||||
import { CCDFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MinimizeRmsd } from '../../../mol-math/linear-algebra/3d/minimize-rmsd';
|
||||
import { SetUtils } from '../../../mol-util/set';
|
||||
import { TrajectoryHierarchyPresetProvider } from '../../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
import { capitalize } from '../../../mol-util/string';
|
||||
|
||||
const CCDParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
|
||||
representationPresetParams: PD.Optional(PD.Group(StructureRepresentationPresetProvider.CommonParams)),
|
||||
showOriginalCoordinates: PD.Optional(PD.Boolean(true, { description: `Show original coordinates for 'model' and 'ideal' structure and do not align them.` })),
|
||||
shownCoordinateType: PD.Select('ideal', PD.arrayToOptions(['ideal', 'model', 'both'] as const), { description: `What coordinate sets are visible.` }),
|
||||
aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }),
|
||||
...TrajectoryHierarchyPresetProvider.CommonParams(a, plugin)
|
||||
});
|
||||
|
||||
export const ChemicalCompontentTrajectoryHierarchyPreset = TrajectoryHierarchyPresetProvider({
|
||||
id: 'preset-trajectory-ccd',
|
||||
display: {
|
||||
name: 'Chemical Component', group: 'Preset',
|
||||
description: 'Shows molecules from the Chemical Component Dictionary.'
|
||||
},
|
||||
isApplicable: o => {
|
||||
return CCDFormat.is(o.data.representative.sourceData);
|
||||
},
|
||||
params: CCDParams,
|
||||
async apply(trajectory, params, plugin) {
|
||||
const tr = StateObjectRef.resolveAndCheck(plugin.state.data, trajectory)?.obj?.data;
|
||||
if (!tr) return {};
|
||||
|
||||
const builder = plugin.builders.structure;
|
||||
|
||||
const idealModel = await builder.createModel(trajectory, { modelIndex: 0 });
|
||||
const idealModelProperties = await builder.insertModelProperties(idealModel, params.modelProperties, { isCollapsed: true });
|
||||
|
||||
const idealStructure = await builder.createStructure(idealModelProperties || idealModel, { name: 'model', params: {} });
|
||||
const idealStructureProperties = await builder.insertStructureProperties(idealStructure, params.structureProperties);
|
||||
|
||||
const representationPreset = params.representationPreset || ChemicalComponentPreset.id;
|
||||
const representationPresetParams = params.representationPresetParams || {};
|
||||
if (representationPresetParams.ignoreHydrogens === undefined) representationPresetParams.ignoreHydrogens = true;
|
||||
|
||||
// degenerate case where either model or ideal coordinates are missing
|
||||
if (tr.frameCount !== 2) {
|
||||
const coordinateType = CCDFormat.CoordinateType.get(idealModel.obj!.data);
|
||||
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, coordinateType });
|
||||
|
||||
return { models: [idealModel], structures: [idealStructure] };
|
||||
}
|
||||
|
||||
const modelModel = await builder.createModel(trajectory, { modelIndex: 1 });
|
||||
const modelModelProperties = await builder.insertModelProperties(modelModel, params.modelProperties, { isCollapsed: true });
|
||||
|
||||
const modelStructure = await builder.createStructure(modelModelProperties || modelModel, { name: 'model', params: {} });
|
||||
const modelStructureProperties = await builder.insertStructureProperties(modelStructure, params.structureProperties);
|
||||
|
||||
// align ideal and model coordinates
|
||||
if (!params.showOriginalCoordinates) {
|
||||
const [a, b] = getPositionTables(idealStructure.obj!.data, modelStructure.obj!.data);
|
||||
if (!a) {
|
||||
plugin.log.warn(`Cannot align chemical components whose atom sets are disjoint.`);
|
||||
} else {
|
||||
const { bTransform, rmsd } = MinimizeRmsd.compute({ a, b });
|
||||
await transform(plugin, modelStructure.cell!, bTransform);
|
||||
plugin.log.info(`Superposed [model] and [ideal] with RMSD ${rmsd.toFixed(2)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, aromaticBonds: params.aromaticBonds, coordinateType: 'ideal', isHidden: params.shownCoordinateType === 'model' });
|
||||
await builder.representation.applyPreset(modelStructureProperties, representationPreset, { ...representationPresetParams, aromaticBonds: params.aromaticBonds, coordinateType: 'model', isHidden: params.shownCoordinateType === 'ideal' });
|
||||
|
||||
return { models: [idealModel, modelModel], structures: [idealStructure, modelStructure] };
|
||||
}
|
||||
});
|
||||
|
||||
function getPositionTables(s1: Structure, s2: Structure) {
|
||||
const m1 = getAtomIdSerialMap(s1);
|
||||
const m2 = getAtomIdSerialMap(s2);
|
||||
const intersecting = SetUtils.intersection(new Set(m1.keys()), new Set(m2.keys()));
|
||||
|
||||
const ret = [
|
||||
MinimizeRmsd.Positions.empty(intersecting.size),
|
||||
MinimizeRmsd.Positions.empty(intersecting.size)
|
||||
];
|
||||
let o = 0;
|
||||
intersecting.forEach(k => {
|
||||
ret[0].x[o] = s1.model.atomicConformation.x[m1.get(k)!];
|
||||
ret[0].y[o] = s1.model.atomicConformation.y[m1.get(k)!];
|
||||
ret[0].z[o] = s1.model.atomicConformation.z[m1.get(k)!];
|
||||
ret[1].x[o] = s2.model.atomicConformation.x[m2.get(k)!];
|
||||
ret[1].y[o] = s2.model.atomicConformation.y[m2.get(k)!];
|
||||
ret[1].z[o] = s2.model.atomicConformation.z[m2.get(k)!];
|
||||
o++;
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getAtomIdSerialMap(structure: Structure) {
|
||||
const map = new Map<string, number>();
|
||||
const { label_atom_id } = structure.model.atomicHierarchy.atoms;
|
||||
for (let i = 0, il = label_atom_id.rowCount; i < il; ++i) {
|
||||
const id = label_atom_id.value(i);
|
||||
if (!map.has(id)) map.set(id, map.size);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function transform(plugin: PluginContext, s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
|
||||
const b = plugin.state.data.build().to(s)
|
||||
.insert(StateTransforms.Model.TransformStructureConformation, { transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
|
||||
return plugin.runTask(plugin.state.data.updateTree(b));
|
||||
}
|
||||
|
||||
export const ChemicalComponentPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-chemical-component',
|
||||
display: {
|
||||
name: 'Chemical Component', group: 'Miscellaneous',
|
||||
description: `Show 'Ideal' and 'Model' coordinates of chemical components.`
|
||||
},
|
||||
isApplicable: o => {
|
||||
return CCDFormat.is(o.data.model.sourceData);
|
||||
},
|
||||
params: () => ({
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
aromaticBonds: PD.Boolean(true),
|
||||
coordinateType: PD.Select<CCDFormat.CoordinateType>('ideal', PD.arrayToOptions(['ideal', 'model'])),
|
||||
isHidden: PD.Boolean(false),
|
||||
}),
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
if (!structureCell) return {};
|
||||
|
||||
const { aromaticBonds, coordinateType, isHidden } = params;
|
||||
const components = {
|
||||
[coordinateType]: await presetStaticComponent(plugin, structureCell, 'all', { label: capitalize(coordinateType), tags: [coordinateType] })
|
||||
};
|
||||
|
||||
const structure = structureCell.obj!.data;
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
|
||||
const representations = {
|
||||
[coordinateType]: builder.buildRepresentation(update, components[coordinateType], { type: 'ball-and-stick', typeParams: { ...typeParams, aromaticBonds } }, { initialState: { isHidden } }),
|
||||
};
|
||||
// sync UI state
|
||||
if (components[coordinateType]?.cell?.state && isHidden) {
|
||||
StateTransform.assignState(components[coordinateType]!.cell!.state, { isHidden });
|
||||
}
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await StructureRepresentationPresetProvider.updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
301
src/extensions/wwpdb/struct-conn/index.ts
Normal file
301
src/extensions/wwpdb/struct-conn/index.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { StructureComponent } from '../../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
|
||||
import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { MolScriptBuilder } from '../../../mol-script/language/builder';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
|
||||
|
||||
/** Amount by which to expand the camera radius when zooming to atoms involved in struct_conn (angstroms) */
|
||||
const EXTRA_RADIUS = 4;
|
||||
|
||||
/** Tags for state tree nodes managed by this extension */
|
||||
const TAGS = {
|
||||
RESIDUE_SEL: 'structconn-focus-residue-sel',
|
||||
ATOM_SEL: 'structconn-focus-atom-sel',
|
||||
RESIDUE_REPR: 'structconn-focus-residue-repr',
|
||||
RESIDUE_NCI_REPR: 'structconn-focus-residue-nci-repr',
|
||||
ATOM_REPR: 'structconn-focus-atom-repr',
|
||||
} as const;
|
||||
|
||||
type VisualParams = ReturnType<typeof StructureRepresentation3D.createDefaultParams>
|
||||
|
||||
/** Parameters for 3D representation of atoms involved in struct_conn (pink bubbles) */
|
||||
const ATOMS_VISUAL_PARAMS: VisualParams = {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.25, sizeAspectRatio: 0.73, adjustCylinderLength: true, xrayShaded: true, aromaticBonds: false, multipleBonds: 'off', dashCount: 1, dashCap: false } },
|
||||
colorTheme: { name: 'uniform', params: { value: ColorNames.magenta } },
|
||||
sizeTheme: { name: 'physical', params: {} },
|
||||
} as const;
|
||||
|
||||
/** Parameters for 3D representation of residues involved in struct_conn (normal ball-and-stick) */
|
||||
const RESIDUES_VISUAL_PARAMS: VisualParams = {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.16 } },
|
||||
colorTheme: { name: 'element-symbol', params: {} },
|
||||
sizeTheme: { name: 'physical', params: {} },
|
||||
} as const;
|
||||
|
||||
|
||||
/** All public functions provided by the StructConn extension */
|
||||
export const wwPDBStructConnExtensionFunctions = {
|
||||
/** Return an object with all struct_conn records for a loaded structure.
|
||||
* Applies to the first structure belonging to `entry` (e.g. '1tqn'),
|
||||
* or to the first loaded structure overall if `entry` is `undefined`.
|
||||
*/
|
||||
getStructConns(plugin: PluginContext, entry: string | undefined): { [id: string]: StructConnRecord } {
|
||||
const structNode = selectStructureNode(plugin, entry);
|
||||
const structure = structNode?.obj?.data;
|
||||
if (structure) return extractStructConns(structure.model);
|
||||
else return {};
|
||||
},
|
||||
|
||||
/** Create visuals for residues and atoms involved in a struct_conn with ID `structConnId`
|
||||
* and zoom on them. If `keepExisting` is false (default), remove any such visuals created by previous calls to this function.
|
||||
* Also hide all carbohydrate SNFG visuals within the structure (as they would occlude our residues of interest).
|
||||
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
|
||||
*/
|
||||
async inspectStructConn(plugin: PluginContext, entry: string | undefined, structConnId: string, keepExisting: boolean = false): Promise<number> {
|
||||
const structNode = selectStructureNode(plugin, entry);
|
||||
const structure = structNode?.obj?.data;
|
||||
if (!structure) {
|
||||
console.error('Structure not found');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const conns: { [id: string]: StructConnRecord } = structure.model._staticPropertyData['wwpdb-struct-conn-extension-data'] ??= extractStructConns(structure.model);
|
||||
const conn = conns[structConnId];
|
||||
if (!conn) {
|
||||
console.error(`The structure does not contain struct_conn "${structConnId}"`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!keepExisting) {
|
||||
await removeAllStructConnInspections(plugin, structNode);
|
||||
}
|
||||
const nSelectedAtoms = await addStructConnInspection(plugin, structNode, conn);
|
||||
hideSnfgNodes(plugin, structNode);
|
||||
return nSelectedAtoms;
|
||||
},
|
||||
|
||||
/** Remove anything created by `inspectStructConn` within the structure and
|
||||
* make visible any carbohydrate SNFG visuals that have been hidden by `inspectStructConn`.
|
||||
*/
|
||||
async clearStructConnInspections(plugin: PluginContext, entry: string | undefined) {
|
||||
const structNode = selectStructureNode(plugin, entry);
|
||||
if (!structNode) return;
|
||||
await removeAllStructConnInspections(plugin, structNode);
|
||||
unhideSnfgNodes(plugin, structNode);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
type StructNode = Exclude<ReturnType<typeof selectStructureNode>, undefined>
|
||||
|
||||
/** Return the first structure node belonging to `entry` (e.g. '1tqn'),
|
||||
* or to the first loaded structure node overall if `entry` is `undefined`.
|
||||
* Includes only "root" structures, not structure components. */
|
||||
function selectStructureNode(plugin: PluginContext, entry: string | undefined) {
|
||||
const structNodes = plugin.state.data
|
||||
.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
|
||||
if (entry) {
|
||||
const result = structNodes.find(node => node.obj && node.obj.data.model.entry.toLowerCase() === entry.toLowerCase());
|
||||
if (!result) {
|
||||
console.warn(`Structure with entry ID "${entry}" was not found. Available structures: ${structNodes.map(node => node.obj?.data.model.entry)}`);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
if (structNodes.length > 1) {
|
||||
console.warn(`Structure entry ID was not specified, but there is more than one loaded structure (${structNodes.map(node => node.obj?.data.model.entry)}). Taking the first structure.`);
|
||||
}
|
||||
if (structNodes.length === 0) {
|
||||
console.warn(`There are no loaded structures.`);
|
||||
}
|
||||
return structNodes[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Represents one partner (i.e. atom) of a struct_conn */
|
||||
interface StructConnPartner {
|
||||
asymId: string,
|
||||
seqId: number | undefined,
|
||||
authSeqId: number | undefined,
|
||||
insCode: string,
|
||||
compId: string,
|
||||
atomId: string,
|
||||
/** Alternative location (use empty string if not given) */
|
||||
altId: string,
|
||||
}
|
||||
|
||||
/** Represents a struct_conn (interaction between two partners) */
|
||||
export interface StructConnRecord {
|
||||
id: string,
|
||||
distance: number,
|
||||
partner1: StructConnPartner,
|
||||
partner2: StructConnPartner,
|
||||
}
|
||||
|
||||
|
||||
/** Return an object with all struct_conn records read from mmCIF.
|
||||
* Return {} if the model comes from another format than mmCIF.
|
||||
*/
|
||||
function extractStructConns(model: Model): { [id: string]: StructConnRecord } {
|
||||
if (!MmcifFormat.is(model.sourceData)) {
|
||||
console.error('Cannot get struct_conn because source data are not mmCIF.');
|
||||
return {};
|
||||
}
|
||||
const mmcifData = model.sourceData.data;
|
||||
const {
|
||||
id,
|
||||
ptnr1_label_asym_id: asym1,
|
||||
ptnr1_label_seq_id: seq1,
|
||||
ptnr1_auth_seq_id: authSeq1,
|
||||
pdbx_ptnr1_PDB_ins_code: authInsCode1,
|
||||
ptnr1_label_comp_id: comp1,
|
||||
ptnr1_label_atom_id: atom1,
|
||||
pdbx_ptnr1_label_alt_id: alt1,
|
||||
ptnr2_label_asym_id: asym2,
|
||||
ptnr2_label_seq_id: seq2,
|
||||
ptnr2_auth_seq_id: authSeq2,
|
||||
pdbx_ptnr2_PDB_ins_code: authInsCode2,
|
||||
ptnr2_label_comp_id: comp2,
|
||||
ptnr2_label_atom_id: atom2,
|
||||
pdbx_ptnr2_label_alt_id: alt2,
|
||||
pdbx_dist_value: distance } = mmcifData.db.struct_conn;
|
||||
const n = id.rowCount;
|
||||
const result: { [id: string]: StructConnRecord } = {};
|
||||
for (let i = 0; i < n; i++) {
|
||||
const conn: StructConnRecord = {
|
||||
id: id.value(i),
|
||||
distance: distance.value(i),
|
||||
partner1: {
|
||||
asymId: asym1.value(i),
|
||||
seqId: seq1.valueKind(i) === Column.ValueKinds.Present ? seq1.value(i) : undefined,
|
||||
authSeqId: authSeq1.valueKind(i) === Column.ValueKinds.Present ? authSeq1.value(i) : undefined,
|
||||
insCode: authInsCode1.value(i),
|
||||
compId: comp1.value(i),
|
||||
atomId: atom1.value(i),
|
||||
altId: alt1.value(i),
|
||||
},
|
||||
partner2: {
|
||||
asymId: asym2.value(i),
|
||||
seqId: seq2.valueKind(i) === Column.ValueKinds.Present ? seq2.value(i) : undefined,
|
||||
authSeqId: authSeq2.valueKind(i) === Column.ValueKinds.Present ? authSeq2.value(i) : undefined,
|
||||
insCode: authInsCode2.value(i),
|
||||
compId: comp2.value(i),
|
||||
atomId: atom2.value(i),
|
||||
altId: alt2.value(i),
|
||||
},
|
||||
};
|
||||
result[conn.id] = conn;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return MolScript expression for atoms or residues involved in a struct_conn */
|
||||
function structConnExpression(conn: StructConnRecord, by: 'atoms' | 'residues') {
|
||||
const { core, struct } = MolScriptBuilder;
|
||||
const partnerExpressions = [];
|
||||
for (const partner of [conn.partner1, conn.partner2]) {
|
||||
const propTests: Parameters<typeof struct.generator.atomGroups>[0] = {
|
||||
'chain-test': core.rel.eq([struct.atomProperty.macromolecular.label_asym_id(), partner.asymId]),
|
||||
'group-by': struct.atomProperty.core.operatorName(),
|
||||
};
|
||||
if (partner.seqId !== undefined) {
|
||||
propTests['residue-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_seq_id(), partner.seqId]);
|
||||
} else if (partner.authSeqId !== undefined) { // for the case of water and carbohydrates (see 5elb, covale3 vs covale5)
|
||||
propTests['residue-test'] = core.logic.and([
|
||||
core.rel.eq([struct.atomProperty.macromolecular.auth_seq_id(), partner.authSeqId]),
|
||||
core.rel.eq([struct.atomProperty.macromolecular.pdbx_PDB_ins_code(), partner.insCode]),
|
||||
]);
|
||||
}
|
||||
if (by === 'residues' && partner.altId !== '') {
|
||||
propTests['atom-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]);
|
||||
}
|
||||
if (by === 'atoms') {
|
||||
propTests['atom-test'] = core.logic.and([
|
||||
core.rel.eq([struct.atomProperty.macromolecular.label_atom_id(), partner.atomId]),
|
||||
core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]),
|
||||
]);
|
||||
}
|
||||
partnerExpressions.push(struct.filter.first([struct.generator.atomGroups(propTests)]));
|
||||
}
|
||||
return struct.combinator.merge(partnerExpressions.map(e => struct.modifier.union([e])));
|
||||
}
|
||||
|
||||
/** Create visuals for residues and atoms involved in a struct_conn and zoom on them.
|
||||
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
|
||||
*/
|
||||
async function addStructConnInspection(plugin: PluginContext, structNode: StructNode, conn: StructConnRecord): Promise<number> {
|
||||
const expressionByResidues = structConnExpression(conn, 'residues');
|
||||
const expressionByAtoms = structConnExpression(conn, 'atoms');
|
||||
|
||||
const update = plugin.build();
|
||||
update.to(structNode).apply(
|
||||
StructureComponent,
|
||||
{ label: `${conn.id} (residues)`, type: { name: 'expression', params: expressionByResidues } },
|
||||
{ tags: [TAGS.RESIDUE_SEL] }
|
||||
).apply(
|
||||
StructureRepresentation3D,
|
||||
RESIDUES_VISUAL_PARAMS,
|
||||
{ tags: [TAGS.RESIDUE_REPR] }
|
||||
);
|
||||
|
||||
const atomsSelection = update.to(structNode).apply(
|
||||
StructureComponent,
|
||||
{ label: `${conn.id} (atoms)`, type: { name: 'expression', params: expressionByAtoms } },
|
||||
{ tags: [TAGS.ATOM_SEL] }
|
||||
);
|
||||
const atomsVisual = update.to(atomsSelection.ref).apply(
|
||||
StructureRepresentation3D,
|
||||
ATOMS_VISUAL_PARAMS,
|
||||
{ tags: [TAGS.ATOM_REPR] }
|
||||
);
|
||||
await update.commit();
|
||||
|
||||
plugin.managers.camera.focusRenderObjects(atomsVisual.selector.data?.repr.renderObjects, { extraRadius: EXTRA_RADIUS });
|
||||
const nSelectedAtoms = atomsSelection.selector.obj?.data?.elementCount ?? 0;
|
||||
return nSelectedAtoms;
|
||||
}
|
||||
|
||||
/** Remove anything created by `addStructConnInspection` */
|
||||
async function removeAllStructConnInspections(plugin: PluginContext, structNode: StructNode) {
|
||||
const selNodes = [
|
||||
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.RESIDUE_SEL)),
|
||||
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.ATOM_SEL)),
|
||||
];
|
||||
const update = plugin.build();
|
||||
for (const node of selNodes) {
|
||||
update.delete(node);
|
||||
}
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
/** Hide all carbohydrate SNFG visuals */
|
||||
function hideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
|
||||
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
|
||||
for (const node of snfgNodes) {
|
||||
setSubtreeVisibility(plugin.state.data, node.transform.ref, true); // true means hidden
|
||||
}
|
||||
}
|
||||
|
||||
/** Make visible all carbohydrate SNFG visuals that have been hidden by `hideSnfgNodes` */
|
||||
function unhideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
|
||||
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
|
||||
for (const node of snfgNodes) {
|
||||
try {
|
||||
setSubtreeVisibility(plugin.state.data, node.transform.ref, false); // false means visible
|
||||
} catch {
|
||||
// this is OK, the node has been removed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
|
||||
}));
|
||||
} else if (t.name === 'trajectory') {
|
||||
const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
|
||||
const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
|
||||
const [coordinatesUrl, coordinatesFormat] = t.params.coordinates.split('|');
|
||||
|
||||
await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
|
||||
source: {
|
||||
@@ -216,7 +216,6 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
|
||||
coordinates: {
|
||||
url: coordinatesUrl,
|
||||
format: coordinatesFormat as any,
|
||||
isBinary: coordinatesIsBinary === 'true',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
37
src/mol-canvas3d/_spec/camera.spec.ts
Normal file
37
src/mol-canvas3d/_spec/camera.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Viewport, cameraProject, cameraUnproject } from '../camera/util';
|
||||
|
||||
describe('camera', () => {
|
||||
it('project/unproject', () => {
|
||||
const proj = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
|
||||
const invProj = Mat4.invert(Mat4(), proj);
|
||||
|
||||
const c = Vec4();
|
||||
const po = Vec3();
|
||||
|
||||
const vp = Viewport.create(0, 0, 100, 100);
|
||||
const pi = Vec3.create(0, 0, 1);
|
||||
cameraProject(c, pi, vp, proj);
|
||||
expect(Vec4.equals(c, Vec4.create(50, 50, 2.020202, -1))).toBe(true);
|
||||
cameraUnproject(po, c, vp, invProj);
|
||||
expect(Vec3.equals(po, pi)).toBe(true);
|
||||
|
||||
Vec3.set(pi, 0.5, 0.5, 1);
|
||||
cameraProject(c, pi, vp, proj);
|
||||
cameraUnproject(po, c, vp, invProj);
|
||||
expect(Vec3.equals(po, pi)).toBe(true);
|
||||
|
||||
Viewport.set(vp, 50, 50, 100, 100);
|
||||
Vec3.set(pi, 0.5, 0.5, 1);
|
||||
cameraProject(c, pi, vp, proj);
|
||||
cameraUnproject(po, c, vp, invProj);
|
||||
expect(Vec3.equals(po, pi)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -194,7 +194,7 @@ class Camera implements ICamera {
|
||||
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 bey one pixel
|
||||
// clip position in addition to the one shifted by one pixel
|
||||
this.project(tmpClip, point);
|
||||
this.unproject(tmpPos1, tmpClip);
|
||||
tmpClip[0] += 1;
|
||||
@@ -278,6 +278,7 @@ namespace Camera {
|
||||
fog: 50,
|
||||
clipFar: true,
|
||||
minNear: 5,
|
||||
minFar: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -294,6 +295,7 @@ namespace Camera {
|
||||
fog: number
|
||||
clipFar: boolean
|
||||
minNear: number
|
||||
minFar: number
|
||||
}
|
||||
|
||||
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
|
||||
@@ -311,6 +313,7 @@ namespace Camera {
|
||||
if (typeof source.fog !== 'undefined') out.fog = source.fog;
|
||||
if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
|
||||
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
|
||||
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -323,6 +326,7 @@ namespace Camera {
|
||||
&& a.fog === b.fog
|
||||
&& a.clipFar === b.clipFar
|
||||
&& a.minNear === b.minNear
|
||||
&& a.minFar === b.minFar
|
||||
&& Vec3.exactEquals(a.position, b.position)
|
||||
&& Vec3.exactEquals(a.up, b.up)
|
||||
&& Vec3.exactEquals(a.target, b.target);
|
||||
@@ -390,18 +394,14 @@ function updatePers(camera: Camera) {
|
||||
}
|
||||
|
||||
function updateClip(camera: Camera) {
|
||||
let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
|
||||
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
|
||||
if (radius < 0.01) radius = 0.01;
|
||||
|
||||
const normalizedFar = clipFar ? radius : radiusMax;
|
||||
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
|
||||
const cameraDistance = Vec3.distance(camera.position, camera.target);
|
||||
let near = cameraDistance - radius;
|
||||
let far = cameraDistance + normalizedFar;
|
||||
|
||||
const fogNearFactor = -(50 - fog) / 50;
|
||||
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
|
||||
const fogFar = far;
|
||||
|
||||
if (mode === 'perspective') {
|
||||
// set at least to 5 to avoid slow sphere impostor rendering
|
||||
near = Math.max(Math.min(radiusMax, minNear), near);
|
||||
@@ -417,8 +417,12 @@ function updateClip(camera: Camera) {
|
||||
far = near + 0.01;
|
||||
}
|
||||
|
||||
const fogNearFactor = -(50 - fog) / 50;
|
||||
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
|
||||
const fogFar = far;
|
||||
|
||||
camera.near = near;
|
||||
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
|
||||
camera.far = far;
|
||||
camera.fogNear = fogNear;
|
||||
camera.fogFar = fogFar;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projec
|
||||
|
||||
// transform into window coordinates, set fourth component to 1 / clip.w as in gl_FragCoord.w
|
||||
out[0] = (tmpVec4[0] + 1) * width * 0.5 + x;
|
||||
out[1] = (1 - tmpVec4[1]) * height * 0.5 + y; // flip Y
|
||||
out[1] = (tmpVec4[1] + 1) * height * 0.5 + y;
|
||||
out[2] = (tmpVec4[2] + 1) * 0.5;
|
||||
out[3] = w === 0 ? 0 : 1 / w;
|
||||
return out;
|
||||
@@ -92,7 +92,7 @@ export function cameraUnproject(out: Vec3, point: Vec3 | Vec4, viewport: Viewpor
|
||||
const { x, y, width, height } = viewport;
|
||||
|
||||
const px = point[0] - x;
|
||||
const py = (height - point[1] - 1) - y;
|
||||
const py = point[1] - y;
|
||||
const pz = point[2];
|
||||
|
||||
out[0] = (2 * px) / width - 1;
|
||||
|
||||
@@ -65,7 +65,7 @@ export const Canvas3DParams = {
|
||||
cameraClipping: PD.Group({
|
||||
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
|
||||
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
|
||||
minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
|
||||
minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
|
||||
}, { pivot: 'radius' }),
|
||||
viewport: PD.MappedStatic('canvas', {
|
||||
canvas: PD.Group({}),
|
||||
@@ -107,13 +107,13 @@ export { Canvas3DContext };
|
||||
|
||||
/** Can be used to create multiple Canvas3D objects */
|
||||
interface Canvas3DContext {
|
||||
readonly canvas: HTMLCanvasElement
|
||||
readonly canvas?: HTMLCanvasElement
|
||||
readonly webgl: WebGLContext
|
||||
readonly input: InputObserver
|
||||
readonly passes: Passes
|
||||
readonly attribs: Readonly<Canvas3DContext.Attribs>
|
||||
readonly contextLost: BehaviorSubject<now.Timestamp>
|
||||
readonly contextRestored: BehaviorSubject<now.Timestamp>
|
||||
readonly contextLost?: BehaviorSubject<now.Timestamp>
|
||||
readonly contextRestored?: BehaviorSubject<now.Timestamp>
|
||||
readonly assetManager: AssetManager
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
|
||||
}
|
||||
@@ -332,12 +332,12 @@ namespace Canvas3D {
|
||||
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
|
||||
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
|
||||
|
||||
const controls = TrackballControls.create(input, camera, p.trackball);
|
||||
const controls = TrackballControls.create(input, camera, scene, p.trackball);
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
const helper = new Helper(webgl, scene, p);
|
||||
|
||||
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
|
||||
@@ -445,7 +445,7 @@ namespace Canvas3D {
|
||||
if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
|
||||
const ctx = { renderer, camera: cam, scene, helper };
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating;
|
||||
const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
|
||||
multiSampleHelper.render(ctx, p, true, forceOn);
|
||||
} else {
|
||||
passes.draw.render(ctx, p, true);
|
||||
@@ -615,22 +615,32 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
function consoleStats() {
|
||||
console.table(scene.renderables.map(r => ({
|
||||
const items = scene.renderables.map(r => ({
|
||||
drawCount: r.values.drawCount.ref.value,
|
||||
instanceCount: r.values.instanceCount.ref.value,
|
||||
materialId: r.materialId,
|
||||
renderItemId: r.id,
|
||||
})));
|
||||
console.log(webgl.stats);
|
||||
}));
|
||||
|
||||
console.groupCollapsed(`${items.length} RenderItems`);
|
||||
|
||||
if (items.length < 50) {
|
||||
console.table(items);
|
||||
} else {
|
||||
console.log(items);
|
||||
}
|
||||
console.log(JSON.stringify(webgl.stats, undefined, 4));
|
||||
|
||||
const { texture, attribute, elements } = webgl.resources.getByteCounts();
|
||||
console.log({
|
||||
console.log(JSON.stringify({
|
||||
texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
|
||||
attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
|
||||
elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
|
||||
});
|
||||
}, undefined, 4));
|
||||
|
||||
console.log(webgl.timer.formatedStats());
|
||||
console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4));
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function add(repr: Representation.Any) {
|
||||
@@ -908,6 +918,7 @@ namespace Canvas3D {
|
||||
},
|
||||
dispose: () => {
|
||||
contextRestoredSub.unsubscribe();
|
||||
cancelAnimationFrame(animationFrameHandle);
|
||||
|
||||
markBuffer = [];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 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>
|
||||
@@ -10,20 +10,25 @@
|
||||
|
||||
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, 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';
|
||||
import { Binding } from '../../mol-util/binding';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
|
||||
const B = ButtonsType;
|
||||
const M = ModifiersKeys;
|
||||
const Trigger = Binding.Trigger;
|
||||
const Key = Binding.TriggerKey;
|
||||
|
||||
export const DefaultTrackballBindings = {
|
||||
dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
|
||||
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Rotate around z-axis', 'Drag using ${triggers}'),
|
||||
dragPan: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Pan', 'Drag using ${triggers}'),
|
||||
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true, control: true }))], 'Rotate around z-axis (roll)', 'Drag using ${triggers}'),
|
||||
dragPan: Binding([
|
||||
Trigger(B.Flag.Secondary, M.create()),
|
||||
Trigger(B.Flag.Primary, M.create({ control: true }))
|
||||
], 'Pan', 'Drag using ${triggers}'),
|
||||
dragZoom: Binding.Empty,
|
||||
dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
|
||||
dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
|
||||
@@ -31,6 +36,22 @@ export const DefaultTrackballBindings = {
|
||||
scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
|
||||
scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
|
||||
scrollFocusZoom: Binding.Empty,
|
||||
|
||||
keyMoveForward: Binding([Key('KeyW')], 'Move forward', 'Press ${triggers}'),
|
||||
keyMoveBack: Binding([Key('KeyS')], 'Move back', 'Press ${triggers}'),
|
||||
keyMoveLeft: Binding([Key('KeyA')], 'Move left', 'Press ${triggers}'),
|
||||
keyMoveRight: Binding([Key('KeyD')], 'Move right', 'Press ${triggers}'),
|
||||
keyMoveUp: Binding([Key('KeyR')], 'Move up', 'Press ${triggers}'),
|
||||
keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
|
||||
keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
|
||||
keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
|
||||
keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
|
||||
keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
|
||||
keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
|
||||
keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
|
||||
|
||||
boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
|
||||
enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
|
||||
};
|
||||
|
||||
export const TrackballControlsParams = {
|
||||
@@ -39,6 +60,9 @@ export const TrackballControlsParams = {
|
||||
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
|
||||
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
|
||||
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
|
||||
moveSpeed: PD.Numeric(0.75, { min: 0.1, max: 3, step: 0.1 }),
|
||||
boostMoveFactor: PD.Numeric(5.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
flyMode: PD.Boolean(false),
|
||||
|
||||
animate: PD.MappedStatic('off', {
|
||||
off: PD.EmptyGroup(),
|
||||
@@ -82,6 +106,7 @@ export { TrackballControls };
|
||||
interface TrackballControls {
|
||||
readonly viewport: Viewport
|
||||
readonly isAnimating: boolean
|
||||
readonly isMoving: boolean
|
||||
|
||||
readonly props: Readonly<TrackballControlsProps>
|
||||
setProps: (props: Partial<TrackballControlsProps>) => void
|
||||
@@ -92,8 +117,14 @@ interface TrackballControls {
|
||||
dispose: () => void
|
||||
}
|
||||
namespace TrackballControls {
|
||||
export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
||||
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
|
||||
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
||||
const p: TrackballControlsProps = {
|
||||
...PD.getDefaultValues(TrackballControlsParams),
|
||||
...props,
|
||||
// include default bindings for backwards state compatibility
|
||||
bindings: { ...DefaultTrackballBindings, ...props.bindings }
|
||||
};
|
||||
const b = p.bindings;
|
||||
|
||||
const viewport = Viewport.clone(camera.viewport);
|
||||
|
||||
@@ -104,6 +135,11 @@ namespace TrackballControls {
|
||||
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);
|
||||
const lockSub = input.lock.subscribe(onLock);
|
||||
const leaveSub = input.leave.subscribe(onLeave);
|
||||
|
||||
let _isInteracting = false;
|
||||
|
||||
@@ -117,9 +153,12 @@ namespace TrackballControls {
|
||||
const _rotLastAxis = Vec3();
|
||||
let _rotLastAngle = 0;
|
||||
|
||||
const _zRotPrev = Vec2();
|
||||
const _zRotCurr = Vec2();
|
||||
let _zRotLastAngle = 0;
|
||||
const _rollPrev = Vec2();
|
||||
const _rollCurr = Vec2();
|
||||
let _rollLastAngle = 0;
|
||||
|
||||
let _pitchLastAngle = 0;
|
||||
let _yawLastAngle = 0;
|
||||
|
||||
const _zoomStart = Vec2();
|
||||
const _zoomEnd = Vec2();
|
||||
@@ -149,7 +188,7 @@ namespace TrackballControls {
|
||||
return Vec2.set(
|
||||
mouseOnCircleVec2,
|
||||
(pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
|
||||
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
|
||||
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // viewport.width intentional
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,26 +242,74 @@ namespace TrackballControls {
|
||||
Vec2.copy(_rotPrev, _rotCurr);
|
||||
}
|
||||
|
||||
const zRotQuat = Quat();
|
||||
const rollQuat = Quat();
|
||||
const rollDir = Vec3();
|
||||
|
||||
function zRotateCamera() {
|
||||
const dx = _zRotCurr[0] - _zRotPrev[0];
|
||||
const dy = _zRotCurr[1] - _zRotPrev[1];
|
||||
const angle = p.rotateSpeed * (-dx + dy) * -0.05;
|
||||
function rollCamera() {
|
||||
const k = (keyState.rollRight - keyState.rollLeft) / 45;
|
||||
const dx = (_rollCurr[0] - _rollPrev[0]) * -Math.sign(_rollCurr[1]);
|
||||
const dy = (_rollCurr[1] - _rollPrev[1]) * -Math.sign(_rollCurr[0]);
|
||||
const angle = -p.rotateSpeed * (-dx + dy) + k;
|
||||
|
||||
if (angle) {
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
Quat.setAxisAngle(zRotQuat, _eye, angle);
|
||||
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
|
||||
_zRotLastAngle = angle;
|
||||
} else if (!p.staticMoving && _zRotLastAngle) {
|
||||
_zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle);
|
||||
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
|
||||
Vec3.normalize(rollDir, _eye);
|
||||
Quat.setAxisAngle(rollQuat, rollDir, angle);
|
||||
Vec3.transformQuat(camera.up, camera.up, rollQuat);
|
||||
_rollLastAngle = angle;
|
||||
} else if (!p.staticMoving && _rollLastAngle) {
|
||||
_rollLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
||||
Vec3.normalize(rollDir, _eye);
|
||||
Quat.setAxisAngle(rollQuat, rollDir, _rollLastAngle);
|
||||
Vec3.transformQuat(camera.up, camera.up, rollQuat);
|
||||
}
|
||||
|
||||
Vec2.copy(_zRotPrev, _zRotCurr);
|
||||
Vec2.copy(_rollPrev, _rollCurr);
|
||||
}
|
||||
|
||||
const pitchQuat = Quat();
|
||||
const pitchDir = Vec3();
|
||||
|
||||
function pitchCamera() {
|
||||
const m = (keyState.pitchUp - keyState.pitchDown) / (p.flyMode ? 360 : 90);
|
||||
const angle = -p.rotateSpeed * m;
|
||||
|
||||
if (angle) {
|
||||
Vec3.cross(pitchDir, _eye, camera.up);
|
||||
Vec3.normalize(pitchDir, pitchDir);
|
||||
Quat.setAxisAngle(pitchQuat, pitchDir, angle);
|
||||
Vec3.transformQuat(_eye, _eye, pitchQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
|
||||
_pitchLastAngle = angle;
|
||||
} else if (!p.staticMoving && _pitchLastAngle) {
|
||||
_pitchLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
||||
Vec3.cross(pitchDir, _eye, camera.up);
|
||||
Vec3.normalize(pitchDir, pitchDir);
|
||||
Quat.setAxisAngle(pitchQuat, pitchDir, _pitchLastAngle);
|
||||
Vec3.transformQuat(_eye, _eye, pitchQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
|
||||
}
|
||||
}
|
||||
|
||||
const yawQuat = Quat();
|
||||
const yawDir = Vec3();
|
||||
|
||||
function yawCamera() {
|
||||
const m = (keyState.yawRight - keyState.yawLeft) / (p.flyMode ? 360 : 90);
|
||||
const angle = -p.rotateSpeed * m;
|
||||
|
||||
if (angle) {
|
||||
Vec3.normalize(yawDir, camera.up);
|
||||
Quat.setAxisAngle(yawQuat, yawDir, angle);
|
||||
Vec3.transformQuat(_eye, _eye, yawQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, yawQuat);
|
||||
_yawLastAngle = angle;
|
||||
} else if (!p.staticMoving && _yawLastAngle) {
|
||||
_yawLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
||||
Vec3.normalize(yawDir, camera.up);
|
||||
Quat.setAxisAngle(yawQuat, yawDir, _yawLastAngle);
|
||||
Vec3.transformQuat(_eye, _eye, yawQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, yawQuat);
|
||||
}
|
||||
}
|
||||
|
||||
function zoomCamera() {
|
||||
@@ -283,6 +370,92 @@ namespace TrackballControls {
|
||||
}
|
||||
}
|
||||
|
||||
const keyState = {
|
||||
moveUp: 0, moveDown: 0, moveLeft: 0, moveRight: 0, moveForward: 0, moveBack: 0,
|
||||
pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0,
|
||||
boostMove: 0,
|
||||
};
|
||||
|
||||
const moveDir = Vec3();
|
||||
const moveEye = Vec3();
|
||||
|
||||
function moveCamera(deltaT: number) {
|
||||
Vec3.sub(moveEye, camera.position, camera.target);
|
||||
const minDistance = Math.max(camera.state.minNear, p.minDistance);
|
||||
Vec3.setMagnitude(moveEye, moveEye, minDistance);
|
||||
|
||||
const moveSpeed = deltaT * (60 / 1000) * p.moveSpeed * (keyState.boostMove === 1 ? p.boostMoveFactor : 1);
|
||||
|
||||
if (keyState.moveForward === 1) {
|
||||
Vec3.normalize(moveDir, moveEye);
|
||||
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
|
||||
const dt = Vec3.distance(camera.target, camera.position);
|
||||
const ds = Vec3.distance(scene.boundingSphereVisible.center, camera.position);
|
||||
if (p.flyMode || input.pointerLock || (dt < minDistance && ds < camera.state.radiusMax)) {
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyState.moveBack === 1) {
|
||||
Vec3.normalize(moveDir, moveEye);
|
||||
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyState.moveLeft === 1) {
|
||||
Vec3.cross(moveDir, moveEye, camera.up);
|
||||
Vec3.normalize(moveDir, moveDir);
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
} else {
|
||||
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, _eye);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyState.moveRight === 1) {
|
||||
Vec3.cross(moveDir, moveEye, camera.up);
|
||||
Vec3.normalize(moveDir, moveDir);
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
} else {
|
||||
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, _eye);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyState.moveUp === 1) {
|
||||
Vec3.normalize(moveDir, camera.up);
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
} else {
|
||||
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, _eye);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyState.moveDown === 1) {
|
||||
Vec3.normalize(moveDir, camera.up);
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
} else {
|
||||
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
|
||||
Vec3.sub(camera.target, camera.position, _eye);
|
||||
}
|
||||
}
|
||||
|
||||
if (p.flyMode || input.pointerLock) {
|
||||
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
|
||||
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the distance between object and target is within the min/max distance
|
||||
* and not too large compared to `camera.state.radiusMax`
|
||||
@@ -319,15 +492,19 @@ namespace TrackballControls {
|
||||
/** Update the object's position, direction and up vectors */
|
||||
function update(t: number) {
|
||||
if (lastUpdated === t) return;
|
||||
|
||||
const deltaT = t - lastUpdated;
|
||||
if (lastUpdated > 0) {
|
||||
if (p.animate.name === 'spin') spin(t - lastUpdated);
|
||||
else if (p.animate.name === 'rock') rock(t - lastUpdated);
|
||||
if (p.animate.name === 'spin') spin(deltaT);
|
||||
else if (p.animate.name === 'rock') rock(deltaT);
|
||||
}
|
||||
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
|
||||
rotateCamera();
|
||||
zRotateCamera();
|
||||
rollCamera();
|
||||
pitchCamera();
|
||||
yawCamera();
|
||||
zoomCamera();
|
||||
focusCamera();
|
||||
panCamera();
|
||||
@@ -335,6 +512,15 @@ namespace TrackballControls {
|
||||
Vec3.add(camera.position, camera.target, _eye);
|
||||
checkDistances();
|
||||
|
||||
if (lastUpdated > 0) {
|
||||
// clamp the maximum step size at 15 frames to avoid too big jumps
|
||||
// TODO: make this a parameter?
|
||||
moveCamera(Math.min(deltaT, 15 * 1000 / 60));
|
||||
}
|
||||
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
checkDistances();
|
||||
|
||||
if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
|
||||
Vec3.copy(lastPosition, camera.position);
|
||||
}
|
||||
@@ -363,24 +549,28 @@ namespace TrackballControls {
|
||||
_isInteracting = true;
|
||||
resetRock(); // start rocking from the center after interactions
|
||||
|
||||
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
|
||||
const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
|
||||
const dragPan = Binding.match(p.bindings.dragPan, buttons, modifiers);
|
||||
const dragZoom = Binding.match(p.bindings.dragZoom, buttons, modifiers);
|
||||
const dragFocus = Binding.match(p.bindings.dragFocus, buttons, modifiers);
|
||||
const dragFocusZoom = Binding.match(p.bindings.dragFocusZoom, buttons, modifiers);
|
||||
const dragRotate = Binding.match(b.dragRotate, buttons, modifiers);
|
||||
const dragRotateZ = Binding.match(b.dragRotateZ, buttons, modifiers);
|
||||
const dragPan = Binding.match(b.dragPan, buttons, modifiers);
|
||||
const dragZoom = Binding.match(b.dragZoom, buttons, modifiers);
|
||||
const dragFocus = Binding.match(b.dragFocus, buttons, modifiers);
|
||||
const dragFocusZoom = Binding.match(b.dragFocusZoom, buttons, modifiers);
|
||||
|
||||
getMouseOnCircle(pageX, pageY);
|
||||
getMouseOnScreen(pageX, pageY);
|
||||
|
||||
const pr = input.pixelRatio;
|
||||
const vx = (x * pr - viewport.width / 2 - viewport.x) / viewport.width;
|
||||
const vy = -(input.height - y * pr - viewport.height / 2 - viewport.y) / viewport.height;
|
||||
|
||||
if (isStart) {
|
||||
if (dragRotate) {
|
||||
Vec2.copy(_rotCurr, mouseOnCircleVec2);
|
||||
Vec2.copy(_rotPrev, _rotCurr);
|
||||
}
|
||||
if (dragRotateZ) {
|
||||
Vec2.copy(_zRotCurr, mouseOnCircleVec2);
|
||||
Vec2.copy(_zRotPrev, _zRotCurr);
|
||||
Vec2.set(_rollCurr, vx, vy);
|
||||
Vec2.copy(_rollPrev, _rollCurr);
|
||||
}
|
||||
if (dragZoom || dragFocusZoom) {
|
||||
Vec2.copy(_zoomStart, mouseOnScreenVec2);
|
||||
@@ -397,7 +587,7 @@ namespace TrackballControls {
|
||||
}
|
||||
|
||||
if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
|
||||
if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2);
|
||||
if (dragRotateZ) Vec2.set(_rollCurr, vx, vy);
|
||||
if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
|
||||
if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
|
||||
if (dragFocusZoom) {
|
||||
@@ -418,16 +608,16 @@ namespace TrackballControls {
|
||||
if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
|
||||
else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
|
||||
|
||||
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
|
||||
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
|
||||
_zoomEnd[1] += delta;
|
||||
}
|
||||
if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
|
||||
if (Binding.match(b.scrollFocus, buttons, modifiers)) {
|
||||
_focusEnd[1] += delta;
|
||||
}
|
||||
}
|
||||
|
||||
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
|
||||
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
|
||||
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
|
||||
_isInteracting = true;
|
||||
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
|
||||
}
|
||||
@@ -438,6 +628,177 @@ namespace TrackballControls {
|
||||
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
|
||||
}
|
||||
|
||||
function onMove({ movementX, movementY }: MoveInput) {
|
||||
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
|
||||
|
||||
const cx = viewport.width * 0.5 - viewport.x;
|
||||
const cy = viewport.height * 0.5 - viewport.y;
|
||||
|
||||
Vec2.copy(_rotPrev, getMouseOnCircle(cx, cy));
|
||||
Vec2.copy(_rotCurr, getMouseOnCircle(movementX + cx, movementY + cy));
|
||||
}
|
||||
|
||||
function onKeyDown({ modifiers, code, key, x, y }: KeyInput) {
|
||||
if (outsideViewport(x, y)) return;
|
||||
|
||||
if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
|
||||
keyState.moveForward = 1;
|
||||
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
|
||||
keyState.moveBack = 1;
|
||||
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
|
||||
keyState.moveLeft = 1;
|
||||
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
|
||||
keyState.moveRight = 1;
|
||||
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
|
||||
keyState.moveUp = 1;
|
||||
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
|
||||
keyState.moveDown = 1;
|
||||
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
|
||||
keyState.rollLeft = 1;
|
||||
} else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
|
||||
keyState.rollRight = 1;
|
||||
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
|
||||
keyState.pitchUp = 1;
|
||||
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
|
||||
keyState.pitchDown = 1;
|
||||
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
|
||||
keyState.yawLeft = 1;
|
||||
} else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
|
||||
keyState.yawRight = 1;
|
||||
}
|
||||
|
||||
if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
|
||||
keyState.boostMove = 1;
|
||||
}
|
||||
|
||||
if (Binding.matchKey(b.enablePointerLock, code, modifiers, key)) {
|
||||
input.requestPointerLock(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp({ modifiers, code, key, x, y }: KeyInput) {
|
||||
if (outsideViewport(x, y)) return;
|
||||
|
||||
let isModifierCode = false;
|
||||
|
||||
if (code.startsWith('Alt')) {
|
||||
isModifierCode = true;
|
||||
modifiers.alt = true;
|
||||
} else if (code.startsWith('Shift')) {
|
||||
isModifierCode = true;
|
||||
modifiers.shift = true;
|
||||
} else if (code.startsWith('Control')) {
|
||||
isModifierCode = true;
|
||||
modifiers.control = true;
|
||||
} else if (code.startsWith('Meta')) {
|
||||
isModifierCode = true;
|
||||
modifiers.meta = true;
|
||||
}
|
||||
|
||||
const codes = [];
|
||||
|
||||
if (isModifierCode) {
|
||||
if (keyState.moveForward) codes.push(b.keyMoveForward.triggers[0]?.code || '');
|
||||
if (keyState.moveBack) codes.push(b.keyMoveBack.triggers[0]?.code || '');
|
||||
if (keyState.moveLeft) codes.push(b.keyMoveLeft.triggers[0]?.code || '');
|
||||
if (keyState.moveRight) codes.push(b.keyMoveRight.triggers[0]?.code || '');
|
||||
if (keyState.moveUp) codes.push(b.keyMoveUp.triggers[0]?.code || '');
|
||||
if (keyState.moveDown) codes.push(b.keyMoveDown.triggers[0]?.code || '');
|
||||
if (keyState.rollLeft) codes.push(b.keyRollLeft.triggers[0]?.code || '');
|
||||
if (keyState.rollRight) codes.push(b.keyRollRight.triggers[0]?.code || '');
|
||||
if (keyState.pitchUp) codes.push(b.keyPitchUp.triggers[0]?.code || '');
|
||||
if (keyState.pitchDown) codes.push(b.keyPitchDown.triggers[0]?.code || '');
|
||||
if (keyState.yawLeft) codes.push(b.keyYawLeft.triggers[0]?.code || '');
|
||||
if (keyState.yawRight) codes.push(b.keyYawRight.triggers[0]?.code || '');
|
||||
} else {
|
||||
codes.push(code);
|
||||
}
|
||||
|
||||
for (const code of codes) {
|
||||
if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
|
||||
keyState.moveForward = 0;
|
||||
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
|
||||
keyState.moveBack = 0;
|
||||
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
|
||||
keyState.moveLeft = 0;
|
||||
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
|
||||
keyState.moveRight = 0;
|
||||
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
|
||||
keyState.moveUp = 0;
|
||||
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
|
||||
keyState.moveDown = 0;
|
||||
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
|
||||
keyState.rollLeft = 0;
|
||||
} else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
|
||||
keyState.rollRight = 0;
|
||||
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
|
||||
keyState.pitchUp = 0;
|
||||
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
|
||||
keyState.pitchDown = 0;
|
||||
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
|
||||
keyState.yawLeft = 0;
|
||||
} else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
|
||||
keyState.yawRight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
|
||||
keyState.boostMove = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function initCameraMove() {
|
||||
Vec3.sub(moveEye, camera.position, camera.target);
|
||||
const minDistance = Math.max(camera.state.minNear, p.minDistance);
|
||||
Vec3.setMagnitude(moveEye, moveEye, minDistance);
|
||||
Vec3.sub(camera.target, camera.position, moveEye);
|
||||
|
||||
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
|
||||
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
|
||||
}
|
||||
|
||||
function resetCameraMove() {
|
||||
const { center, radius } = scene.boundingSphereVisible;
|
||||
const cameraDistance = Vec3.distance(camera.position, center);
|
||||
if (cameraDistance > radius) {
|
||||
const focus = camera.getFocus(center, radius);
|
||||
camera.setState({ ...focus, minFar: 0 });
|
||||
} else {
|
||||
camera.setState({
|
||||
minFar: 0,
|
||||
radius: scene.boundingSphereVisible.radius,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onLock(isLocked: boolean) {
|
||||
if (isLocked) {
|
||||
initCameraMove();
|
||||
} else {
|
||||
resetCameraMove();
|
||||
}
|
||||
}
|
||||
|
||||
function unsetKeyState() {
|
||||
keyState.moveForward = 0;
|
||||
keyState.moveBack = 0;
|
||||
keyState.moveLeft = 0;
|
||||
keyState.moveRight = 0;
|
||||
keyState.moveUp = 0;
|
||||
keyState.moveDown = 0;
|
||||
keyState.rollLeft = 0;
|
||||
keyState.rollRight = 0;
|
||||
keyState.pitchUp = 0;
|
||||
keyState.pitchDown = 0;
|
||||
keyState.yawLeft = 0;
|
||||
keyState.yawRight = 0;
|
||||
keyState.boostMove = 0;
|
||||
}
|
||||
|
||||
function onLeave() {
|
||||
unsetKeyState();
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
@@ -447,6 +808,11 @@ namespace TrackballControls {
|
||||
pinchSub.unsubscribe();
|
||||
gestureSub.unsubscribe();
|
||||
interactionEndSub.unsubscribe();
|
||||
keyDownSub.unsubscribe();
|
||||
keyUpSub.unsubscribe();
|
||||
moveSub.unsubscribe();
|
||||
lockSub.unsubscribe();
|
||||
leaveSub.unsubscribe();
|
||||
}
|
||||
|
||||
const _spinSpeed = Vec2.create(0.005, 0);
|
||||
@@ -489,13 +855,31 @@ namespace TrackballControls {
|
||||
return {
|
||||
viewport,
|
||||
get isAnimating() { return p.animate.name !== 'off'; },
|
||||
get isMoving() {
|
||||
return (
|
||||
keyState.moveForward === 1 || keyState.moveBack === 1 ||
|
||||
keyState.moveLeft === 1 || keyState.moveRight === 1 ||
|
||||
keyState.moveUp === 1 || keyState.moveDown === 1 ||
|
||||
keyState.rollLeft === 1 || keyState.rollRight === 1 ||
|
||||
keyState.pitchUp === 1 || keyState.pitchDown === 1 ||
|
||||
keyState.yawLeft === 1 || keyState.yawRight === 1
|
||||
);
|
||||
},
|
||||
|
||||
get props() { return p as Readonly<TrackballControlsProps>; },
|
||||
setProps: (props: Partial<TrackballControlsProps>) => {
|
||||
if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
|
||||
resetRock(); // start rocking from the center
|
||||
}
|
||||
if (props.flyMode !== undefined && props.flyMode !== p.flyMode) {
|
||||
if (props.flyMode) {
|
||||
initCameraMove();
|
||||
} else {
|
||||
resetCameraMove();
|
||||
}
|
||||
}
|
||||
Object.assign(p, props);
|
||||
Object.assign(b, props.bindings);
|
||||
},
|
||||
|
||||
start,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -13,6 +13,7 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Camera } from '../camera';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Bond } from '../../mol-model/structure';
|
||||
import { TrackballControls } from '../controls/trackball';
|
||||
|
||||
type Canvas3D = import('../canvas3d').Canvas3D
|
||||
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
|
||||
@@ -68,7 +69,7 @@ export class Canvas3dInteractionHelper {
|
||||
}
|
||||
|
||||
private identify(e: InputEvent, t: number) {
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
|
||||
|
||||
if (e === InputEvent.Drag) {
|
||||
if (xyChanged && !this.outsideViewport(this.startX, this.startY)) {
|
||||
@@ -188,7 +189,7 @@ export class Canvas3dInteractionHelper {
|
||||
this.ev.dispose();
|
||||
}
|
||||
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) {
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
|
||||
this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
|
||||
|
||||
input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
|
||||
@@ -197,8 +198,12 @@ export class Canvas3dInteractionHelper {
|
||||
this.drag(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
input.move.subscribe(({ x, y, inside, buttons, button, modifiers }) => {
|
||||
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
|
||||
if (!inside || this.isInteracting) return;
|
||||
if (!onElement) {
|
||||
this.leave();
|
||||
return;
|
||||
}
|
||||
// console.log('move');
|
||||
this.move(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -23,6 +23,7 @@ import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Asset, AssetManager } from '../../mol-util/assets';
|
||||
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
|
||||
import { isPowerOfTwo } from '../../mol-math/misc';
|
||||
|
||||
const SharedParams = {
|
||||
opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
@@ -59,6 +60,7 @@ const ImageParams = {
|
||||
url: PD.Text(''),
|
||||
file: PD.File({ accept: 'image/*' }),
|
||||
}),
|
||||
blur: PD.Numeric(0, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'Note, this only works in WebGL2 or with power-of-two images and when "EXT_shader_texture_lod" is available.' }),
|
||||
...SharedParams,
|
||||
coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
|
||||
};
|
||||
@@ -207,6 +209,7 @@ export class BackgroundPass {
|
||||
}
|
||||
if (!this.image) return;
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBlur, props.blur);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
|
||||
@@ -369,6 +372,12 @@ function getSkyboxTexture(ctx: WebGLContext, assetManager: AssetManager, faces:
|
||||
const cubeAssets = getCubeAssets(assetManager, faces);
|
||||
const cubeFaces = getCubeFaces(assetManager, cubeAssets);
|
||||
const assets = [cubeAssets.nx, cubeAssets.ny, cubeAssets.nz, cubeAssets.px, cubeAssets.py, cubeAssets.pz];
|
||||
if (typeof HTMLImageElement === 'undefined') {
|
||||
console.error(`Missing "HTMLImageElement" required for background skybox`);
|
||||
onload?.(true);
|
||||
return { texture: createNullTexture(), assets };
|
||||
}
|
||||
|
||||
const texture = ctx.resources.cubeTexture(cubeFaces, true, onload);
|
||||
return { texture, assets };
|
||||
}
|
||||
@@ -390,18 +399,28 @@ function areImageTexturePropsEqual(sourceA: ImageProps['source'], sourceB: Image
|
||||
}
|
||||
|
||||
function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source: ImageProps['source'], onload?: (errored?: boolean) => void): { texture: Texture, asset: Asset } {
|
||||
const asset = source.name === 'url'
|
||||
? Asset.getUrlAsset(assetManager, source.params)
|
||||
: source.params!;
|
||||
if (typeof HTMLImageElement === 'undefined') {
|
||||
console.error(`Missing "HTMLImageElement" required for background image`);
|
||||
onload?.(true);
|
||||
return { texture: createNullTexture(), asset };
|
||||
}
|
||||
|
||||
const texture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
texture.load(img);
|
||||
if (ctx.isWebGL2 || (isPowerOfTwo(img.width) && isPowerOfTwo(img.height))) {
|
||||
texture.mipmap();
|
||||
}
|
||||
onload?.();
|
||||
};
|
||||
img.onerror = () => {
|
||||
onload?.(true);
|
||||
};
|
||||
const asset = source.name === 'url'
|
||||
? Asset.getUrlAsset(assetManager, source.params)
|
||||
: source.params!;
|
||||
|
||||
assetManager.resolve(asset, 'binary').run().then(a => {
|
||||
const blob = new Blob([a.data]);
|
||||
img.src = URL.createObjectURL(blob);
|
||||
|
||||
120
src/mol-canvas3d/passes/cas.ts
Normal file
120
src/mol-canvas3d/passes/cas.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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 { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { cas_frag } from '../../mol-gl/shader/cas.frag';
|
||||
|
||||
export const CasParams = {
|
||||
sharpness: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }),
|
||||
denoise: PD.Boolean(true),
|
||||
};
|
||||
export type CasProps = PD.Values<typeof CasParams>
|
||||
|
||||
export class CasPass {
|
||||
private readonly renderable: CasRenderable;
|
||||
|
||||
constructor(private webgl: WebGLContext, input: Texture) {
|
||||
this.renderable = getCasRenderable(webgl, input);
|
||||
}
|
||||
|
||||
private updateState(viewport: Viewport) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
|
||||
const { x, y, width, height } = viewport;
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
ValueCell.update(this.renderable.values.uTexSizeInv, Vec2.set(this.renderable.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
|
||||
}
|
||||
|
||||
update(input: Texture, props: CasProps) {
|
||||
const { values } = this.renderable;
|
||||
const { sharpness, denoise } = props;
|
||||
|
||||
let needsUpdate = false;
|
||||
|
||||
if (values.tColor.ref.value !== input) {
|
||||
ValueCell.update(this.renderable.values.tColor, input);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(values.uSharpness, 2 - 2 * Math.pow(sharpness, 0.25));
|
||||
|
||||
if (values.dDenoise.ref.value !== denoise) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dDenoise, denoise);
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(viewport: Viewport, target: RenderTarget | undefined) {
|
||||
if (isTimingMode) this.webgl.timer.mark('CasPass.render');
|
||||
if (target) {
|
||||
target.bind();
|
||||
} else {
|
||||
this.webgl.unbindFramebuffer();
|
||||
}
|
||||
this.updateState(viewport);
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('CasPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const CasSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
uTexSizeInv: UniformSpec('v2'),
|
||||
|
||||
uSharpness: UniformSpec('f'),
|
||||
dDenoise: DefineSpec('boolean'),
|
||||
};
|
||||
const CasShaderCode = ShaderCode('cas', quad_vert, cas_frag);
|
||||
type CasRenderable = ComputeRenderable<Values<typeof CasSchema>>
|
||||
|
||||
function getCasRenderable(ctx: WebGLContext, colorTexture: Texture): CasRenderable {
|
||||
const width = colorTexture.getWidth();
|
||||
const height = colorTexture.getHeight();
|
||||
|
||||
const values: Values<typeof CasSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
|
||||
|
||||
uSharpness: ValueCell.create(0.5),
|
||||
dDenoise: ValueCell.create(true),
|
||||
};
|
||||
|
||||
const schema = { ...CasSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', CasShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -115,19 +115,19 @@ export class DrawPass {
|
||||
|
||||
ValueCell.update(this.copyFboTarget.values.uTexSize, Vec2.set(this.copyFboTarget.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.copyFboPostprocessing.values.uTexSize, Vec2.set(this.copyFboPostprocessing.values.uTexSize.ref.value, width, height));
|
||||
|
||||
if (this.wboit?.supported) {
|
||||
this.wboit.setSize(width, height);
|
||||
}
|
||||
|
||||
if (this.dpoit?.supported) {
|
||||
this.dpoit.setSize(width, height);
|
||||
}
|
||||
|
||||
this.marking.setSize(width, height);
|
||||
this.postprocessing.setSize(width, height);
|
||||
this.antialiasing.setSize(width, height);
|
||||
}
|
||||
|
||||
if (this.wboit?.supported) {
|
||||
this.wboit.setSize(width, height);
|
||||
}
|
||||
|
||||
if (this.dpoit?.supported) {
|
||||
this.dpoit.setSize(width, height);
|
||||
}
|
||||
|
||||
this.marking.setSize(width, height);
|
||||
this.postprocessing.setSize(width, height);
|
||||
this.antialiasing.setSize(width, height);
|
||||
}
|
||||
|
||||
private _renderDpoit(renderer: Renderer, camera: ICamera, scene: Scene, iterations: number, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
|
||||
|
||||
@@ -106,7 +106,7 @@ export class MarkingPass {
|
||||
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength, highlightEdgeStrength, selectEdgeStrength } = props;
|
||||
|
||||
const { values: edgeValues } = this.edge;
|
||||
const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
|
||||
const _edgeScale = Math.max(1, Math.round(edgeScale * this.webgl.pixelRatio));
|
||||
if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
|
||||
ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
|
||||
this.edge.update();
|
||||
|
||||
@@ -53,6 +53,7 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): Compose
|
||||
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.' }),
|
||||
};
|
||||
export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ export class PickHelper {
|
||||
|
||||
const z = this.getDepth(xp, yp);
|
||||
// console.log('z', z);
|
||||
const position = Vec3.create(x, viewport.height - y, z);
|
||||
const position = Vec3.create(x, y, z);
|
||||
if (StereoCamera.is(camera)) {
|
||||
const halfWidth = Math.floor(viewport.width / 2);
|
||||
if (x > viewport.x + halfWidth) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/rende
|
||||
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 { 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';
|
||||
@@ -33,6 +33,7 @@ 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';
|
||||
|
||||
const OutlinesSchema = {
|
||||
...QuadSchema,
|
||||
@@ -43,9 +44,9 @@ const OutlinesSchema = {
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
|
||||
uMaxPossibleViewZDiff: UniformSpec('f'),
|
||||
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
};
|
||||
type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
|
||||
@@ -63,9 +64,9 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
|
||||
uMaxPossibleViewZDiff: ValueCell.create(0.5),
|
||||
|
||||
uOutlineThreshold: ValueCell.create(0.33),
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
};
|
||||
|
||||
@@ -137,6 +138,8 @@ function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): Shadows
|
||||
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'),
|
||||
@@ -149,14 +152,23 @@ const SsaoSchema = {
|
||||
|
||||
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): SsaoRenderable {
|
||||
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),
|
||||
@@ -167,8 +179,15 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
uRadius: ValueCell.create(8.0),
|
||||
uBias: ValueCell.create(0.025),
|
||||
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 };
|
||||
@@ -189,8 +208,7 @@ const SsaoBlurSchema = {
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
|
||||
uMaxPossibleViewZDiff: UniformSpec('f'),
|
||||
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
@@ -211,8 +229,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
uMaxPossibleViewZDiff: ValueCell.create(0.5),
|
||||
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
@@ -280,11 +297,9 @@ const PostprocessingSchema = {
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
uOutlineColor: UniformSpec('v3'),
|
||||
uOcclusionColor: UniformSpec('v3'),
|
||||
uTransparentBackground: UniformSpec('b'),
|
||||
|
||||
uMaxPossibleViewZDiff: UniformSpec('f'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
|
||||
dOcclusionEnable: DefineSpec('boolean'),
|
||||
uOcclusionOffset: UniformSpec('v2'),
|
||||
|
||||
@@ -292,13 +307,10 @@ const PostprocessingSchema = {
|
||||
|
||||
dOutlineEnable: DefineSpec('boolean'),
|
||||
dOutlineScale: DefineSpec('number'),
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
};
|
||||
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 {
|
||||
const values: Values<typeof PostprocessingSchema> = {
|
||||
...QuadValues,
|
||||
@@ -317,11 +329,9 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
|
||||
uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
|
||||
uTransparentBackground: ValueCell.create(false),
|
||||
|
||||
uMaxPossibleViewZDiff: ValueCell.create(0.5),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
|
||||
dOcclusionEnable: ValueCell.create(true),
|
||||
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
|
||||
|
||||
@@ -329,8 +339,6 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
|
||||
|
||||
dOutlineEnable: ValueCell.create(false),
|
||||
dOutlineScale: ValueCell.create(1),
|
||||
uOutlineThreshold: ValueCell.create(0.33),
|
||||
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
};
|
||||
|
||||
@@ -345,10 +353,27 @@ export const PostprocessingParams = {
|
||||
occlusion: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
|
||||
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)),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
|
||||
@@ -375,11 +400,36 @@ export const PostprocessingParams = {
|
||||
smaa: PD.Group(SmaaParams),
|
||||
off: PD.Group({})
|
||||
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
|
||||
sharpening: PD.MappedStatic('off', {
|
||||
on: PD.Group(CasParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Contrast Adaptive Sharpening' }),
|
||||
background: PD.Group(BackgroundParams, { isFlat: true }),
|
||||
};
|
||||
|
||||
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';
|
||||
@@ -404,6 +454,12 @@ export class PostprocessingPass {
|
||||
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;
|
||||
|
||||
@@ -413,16 +469,17 @@ export class PostprocessingPass {
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private downsampleFactor: number;
|
||||
|
||||
private readonly renderable: PostprocessingRenderable;
|
||||
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale() {
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
|
||||
}
|
||||
|
||||
private levels: { radius: number, bias: number }[];
|
||||
|
||||
private readonly bgColor = Vec3();
|
||||
readonly background: BackgroundPass;
|
||||
|
||||
@@ -433,8 +490,8 @@ export class PostprocessingPass {
|
||||
|
||||
this.nSamples = 1;
|
||||
this.blurKernelSize = 1;
|
||||
this.downsampleFactor = 1;
|
||||
this.ssaoScale = this.calcSsaoScale();
|
||||
this.ssaoScale = this.calcSsaoScale(1);
|
||||
this.levels = [];
|
||||
|
||||
// needs to be linear for anti-aliasing pass
|
||||
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
@@ -452,11 +509,29 @@ export class PostprocessingPass {
|
||||
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', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', 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', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
|
||||
|
||||
this.depthQuarterTarget = drawPass.packedDepth
|
||||
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', 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.ssaoFramebuffer, 'color0');
|
||||
@@ -467,7 +542,7 @@ export class PostprocessingPass {
|
||||
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
|
||||
|
||||
this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
|
||||
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);
|
||||
@@ -477,28 +552,46 @@ export class PostprocessingPass {
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const ssaoScale = this.calcSsaoScale();
|
||||
const ssaoScale = this.calcSsaoScale(1);
|
||||
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -508,6 +601,7 @@ export class PostprocessingPass {
|
||||
let needsUpdateMain = false;
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
let needsUpdateOutlines = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
@@ -543,11 +637,14 @@ export class PostprocessingPass {
|
||||
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);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
|
||||
|
||||
if (this.nSamples !== props.occlusion.params.samples) {
|
||||
needsUpdateSsao = true;
|
||||
@@ -556,7 +653,30 @@ export class PostprocessingPass {
|
||||
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
|
||||
|
||||
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) {
|
||||
@@ -567,34 +687,47 @@ export class PostprocessingPass {
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
}
|
||||
|
||||
if (this.downsampleFactor !== props.occlusion.params.resolutionScale) {
|
||||
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
|
||||
if (this.ssaoScale !== ssaoScale) {
|
||||
needsUpdateSsao = true;
|
||||
needsUpdateDepthHalf = true;
|
||||
|
||||
this.downsampleFactor = props.occlusion.params.resolutionScale;
|
||||
this.ssaoScale = this.calcSsaoScale();
|
||||
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);
|
||||
|
||||
if (this.ssaoScale === 1) {
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureOpaque);
|
||||
} else {
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
|
||||
}
|
||||
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 (props.shadow.name === 'on') {
|
||||
@@ -611,7 +744,10 @@ export class PostprocessingPass {
|
||||
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.dOrthographic, orthographic);
|
||||
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);
|
||||
@@ -630,30 +766,33 @@ export class PostprocessingPass {
|
||||
}
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
let { threshold, includeTransparent } = props.outline.params;
|
||||
const transparentOutline = includeTransparent ?? true;
|
||||
// orthographic needs lower threshold
|
||||
if (camera.state.mode === 'orthographic') threshold /= 5;
|
||||
const factor = Math.pow(1000, threshold / 10) / 1000;
|
||||
// use radiusMax for stable outlines when zooming
|
||||
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
|
||||
const outlineScale = props.outline.params.scale - 1;
|
||||
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;
|
||||
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
|
||||
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateOutlines = true; }
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
|
||||
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.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
|
||||
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
|
||||
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dTransparentOutline, transparentOutline);
|
||||
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOutlineScale, outlineScale);
|
||||
}
|
||||
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
|
||||
}
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
@@ -662,15 +801,23 @@ 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.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
|
||||
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
|
||||
}
|
||||
|
||||
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
|
||||
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dShadowEnable, shadowsEnabled);
|
||||
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled);
|
||||
}
|
||||
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled);
|
||||
}
|
||||
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
if (needsUpdateOutlines) {
|
||||
this.outlinesRenderable.update();
|
||||
@@ -689,6 +836,10 @@ export class PostprocessingPass {
|
||||
this.ssaoBlurSecondPassRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateMain) {
|
||||
this.renderable.update();
|
||||
}
|
||||
@@ -699,10 +850,6 @@ export class PostprocessingPass {
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
}
|
||||
|
||||
private occlusionOffset: [x: number, y: number] = [0, 0];
|
||||
@@ -721,25 +868,38 @@ export class PostprocessingPass {
|
||||
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
|
||||
this.updateState(camera, transparentBackground, backgroundColor, props, light);
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
this.outlinesTarget.bind();
|
||||
this.outlinesRenderable.render();
|
||||
}
|
||||
|
||||
if (props.shadow.name === 'on') {
|
||||
this.shadowsTarget.bind();
|
||||
this.shadowsRenderable.render();
|
||||
}
|
||||
const { gl, 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();
|
||||
|
||||
@@ -751,14 +911,25 @@ export class PostprocessingPass {
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
|
||||
}
|
||||
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
this.outlinesTarget.bind();
|
||||
this.outlinesRenderable.render();
|
||||
}
|
||||
|
||||
if (props.shadow.name === 'on') {
|
||||
this.shadowsTarget.bind();
|
||||
this.shadowsRenderable.render();
|
||||
}
|
||||
|
||||
if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.target.bind();
|
||||
}
|
||||
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
this.background.update(camera, props.background);
|
||||
if (this.background.isEnabled(props.background)) {
|
||||
if (this.transparentBackground) {
|
||||
@@ -787,8 +958,11 @@ export class AntialiasingPass {
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
private readonly internalTarget: RenderTarget;
|
||||
|
||||
private readonly fxaa: FxaaPass;
|
||||
private readonly smaa: SmaaPass;
|
||||
private readonly cas: CasPass;
|
||||
|
||||
constructor(webgl: WebGLContext, private drawPass: DrawPass) {
|
||||
const { colorTarget } = drawPass;
|
||||
@@ -796,8 +970,11 @@ export class AntialiasingPass {
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
this.internalTarget = webgl.createRenderTarget(width, height, false);
|
||||
|
||||
this.fxaa = new FxaaPass(webgl, this.target.texture);
|
||||
this.smaa = new SmaaPass(webgl, this.target.texture);
|
||||
this.cas = new CasPass(webgl, this.target.texture);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
@@ -806,42 +983,69 @@ export class AntialiasingPass {
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
this.internalTarget.setSize(width, height);
|
||||
this.fxaa.setSize(width, height);
|
||||
if (this.smaa.supported) this.smaa.setSize(width, height);
|
||||
this.cas.setSize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
private _renderFxaa(camera: ICamera, target: RenderTarget | undefined, props: PostprocessingProps) {
|
||||
if (props.antialiasing.name !== 'fxaa') return;
|
||||
|
||||
const input = PostprocessingPass.isEnabled(props)
|
||||
? this.drawPass.postprocessing.target.texture
|
||||
: this.drawPass.colorTarget.texture;
|
||||
this.fxaa.update(input, props.antialiasing.params);
|
||||
this.fxaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target);
|
||||
this.fxaa.render(camera.viewport, target);
|
||||
}
|
||||
|
||||
private _renderSmaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
private _renderSmaa(camera: ICamera, target: RenderTarget | undefined, props: PostprocessingProps) {
|
||||
if (props.antialiasing.name !== 'smaa') return;
|
||||
|
||||
const input = PostprocessingPass.isEnabled(props)
|
||||
? this.drawPass.postprocessing.target.texture
|
||||
: this.drawPass.colorTarget.texture;
|
||||
this.smaa.update(input, props.antialiasing.params);
|
||||
this.smaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target);
|
||||
this.smaa.render(camera.viewport, target);
|
||||
}
|
||||
|
||||
private _renderAntialiasing(camera: ICamera, target: RenderTarget | undefined, props: PostprocessingProps) {
|
||||
if (props.antialiasing.name === 'fxaa') {
|
||||
this._renderFxaa(camera, target, props);
|
||||
} else if (props.antialiasing.name === 'smaa') {
|
||||
this._renderSmaa(camera, target, props);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderCas(camera: ICamera, target: RenderTarget | undefined, props: PostprocessingProps) {
|
||||
if (props.sharpening.name !== 'on') return;
|
||||
|
||||
const input = props.antialiasing.name !== 'off'
|
||||
? this.internalTarget.texture
|
||||
: PostprocessingPass.isEnabled(props)
|
||||
? this.drawPass.postprocessing.target.texture
|
||||
: this.drawPass.colorTarget.texture;
|
||||
this.cas.update(input, props.sharpening.params);
|
||||
this.cas.render(camera.viewport, target);
|
||||
}
|
||||
|
||||
render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
if (props.antialiasing.name === 'off') return;
|
||||
if (props.antialiasing.name === 'off' && props.sharpening.name === 'off') return;
|
||||
|
||||
if (props.antialiasing.name === 'fxaa') {
|
||||
this._renderFxaa(camera, toDrawingBuffer, props);
|
||||
} else if (props.antialiasing.name === 'smaa') {
|
||||
if (!this.smaa.supported) {
|
||||
throw new Error('SMAA not supported, missing "HTMLImageElement"');
|
||||
}
|
||||
this._renderSmaa(camera, toDrawingBuffer, props);
|
||||
if (props.antialiasing.name === 'smaa' && !this.smaa.supported) {
|
||||
console.error('SMAA not supported, missing "HTMLImageElement"');
|
||||
return;
|
||||
}
|
||||
|
||||
const target = toDrawingBuffer ? undefined : this.target;
|
||||
if (props.sharpening.name === 'off') {
|
||||
this._renderAntialiasing(camera, target, props);
|
||||
} else if (props.antialiasing.name === 'off') {
|
||||
this._renderCas(camera, target, props);
|
||||
} else {
|
||||
this._renderAntialiasing(camera, this.internalTarget, props);
|
||||
this._renderCas(camera, target, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export namespace BaseGeometry {
|
||||
if (!transform) transform = createIdentityTransform();
|
||||
const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
|
||||
const theme: Theme = {
|
||||
color: UniformColorTheme({}, { value: colorValue }),
|
||||
color: UniformColorTheme({}, { value: colorValue, lightness: 0, saturation: 0 }),
|
||||
size: UniformSizeTheme({}, { value: sizeValue })
|
||||
};
|
||||
return { transform, locationIterator, theme };
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2023 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 { ChunkedArray } from '../../../mol-data/util';
|
||||
@@ -10,7 +11,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
export interface CylindersBuilder {
|
||||
add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
|
||||
addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
|
||||
addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number): void
|
||||
addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
|
||||
getCylinders(): Cylinders
|
||||
}
|
||||
@@ -41,19 +42,24 @@ export namespace CylindersBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
|
||||
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number) => {
|
||||
const d = Vec3.distance(start, end);
|
||||
const s = Math.floor(segmentCount / 2);
|
||||
const step = 1 / segmentCount;
|
||||
const isOdd = segmentCount % 2 !== 0;
|
||||
const s = Math.floor((segmentCount + 1) / 2);
|
||||
const step = d / (segmentCount + 0.5);
|
||||
|
||||
Vec3.sub(tmpDir, end, start);
|
||||
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, end, start), step);
|
||||
Vec3.copy(tmpVecA, start);
|
||||
for (let j = 0; j < s; ++j) {
|
||||
const f = step * (j * 2 + 1);
|
||||
Vec3.setMagnitude(tmpDir, tmpDir, d * f);
|
||||
Vec3.add(tmpVecA, start, tmpDir);
|
||||
Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
|
||||
Vec3.add(tmpVecB, start, tmpDir);
|
||||
Vec3.add(tmpVecA, tmpVecA, tmpDir);
|
||||
if (isOdd && j === s - 1) {
|
||||
Vec3.copy(tmpVecB, end);
|
||||
if (!stubCap) bottomCap = false;
|
||||
} else {
|
||||
Vec3.add(tmpVecB, tmpVecA, tmpDir);
|
||||
}
|
||||
add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], radiusScale, topCap, bottomCap, group);
|
||||
Vec3.add(tmpVecA, tmpVecA, tmpDir);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +68,7 @@ export namespace CylindersBuilder {
|
||||
addFixedCountDashes,
|
||||
addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
|
||||
const d = Vec3.distance(start, end);
|
||||
addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, group);
|
||||
addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, true, group);
|
||||
},
|
||||
getCylinders: () => {
|
||||
const cylinderCount = groups.elementCount / 6;
|
||||
|
||||
@@ -156,7 +156,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),
|
||||
xrayShaded: 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']), BaseGeometry.ShadingCategory),
|
||||
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
@@ -244,7 +244,7 @@ export namespace Cylinders {
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
|
||||
uDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
@@ -263,7 +263,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.dXrayShaded, props.xrayShaded);
|
||||
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);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
|
||||
@@ -147,7 +147,7 @@ export namespace DirectVolume {
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: 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),
|
||||
Vec2.create(0.79, 0.0), Vec2.create(0.8, 0.05), Vec2.create(0.85, 0.05), Vec2.create(0.86, 0.0),
|
||||
@@ -269,7 +269,7 @@ export namespace DirectVolume {
|
||||
dAxisOrder: ValueCell.create(directVolume.axisOrder.ref.value.join('')),
|
||||
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,7 +282,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.dXrayShaded, props.xrayShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
|
||||
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);
|
||||
createTransferFunctionTexture(controlPoints, values.tTransferTex);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -81,7 +81,7 @@ export namespace Geometry {
|
||||
switch (geometry.kind) {
|
||||
case 'mesh': return geometry.vertexCount;
|
||||
case 'points': return geometry.pointCount;
|
||||
case 'spheres': return geometry.sphereCount * 4;
|
||||
case 'spheres': return geometry.sphereCount * 6;
|
||||
case 'cylinders': return geometry.cylinderCount * 6;
|
||||
case 'text': return geometry.charCount * 4;
|
||||
case 'lines': return geometry.lineCount * 4;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 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 { ChunkedArray } from '../../../mol-data/util';
|
||||
@@ -50,17 +51,21 @@ export namespace LinesBuilder {
|
||||
|
||||
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, group: number) => {
|
||||
const d = Vec3.distance(start, end);
|
||||
const s = Math.floor(segmentCount / 2);
|
||||
const step = 1 / segmentCount;
|
||||
const isOdd = segmentCount % 2 !== 0;
|
||||
const s = Math.floor((segmentCount + 1) / 2);
|
||||
const step = d / (segmentCount + 0.5);
|
||||
|
||||
Vec3.sub(tmpDir, end, start);
|
||||
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, end, start), step);
|
||||
Vec3.copy(tmpVecA, start);
|
||||
for (let j = 0; j < s; ++j) {
|
||||
const f = step * (j * 2 + 1);
|
||||
Vec3.setMagnitude(tmpDir, tmpDir, d * f);
|
||||
Vec3.add(tmpVecA, start, tmpDir);
|
||||
Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
|
||||
Vec3.add(tmpVecB, start, tmpDir);
|
||||
Vec3.add(tmpVecA, tmpVecA, tmpDir);
|
||||
if (isOdd && j === s - 1) {
|
||||
Vec3.copy(tmpVecB, end);
|
||||
} else {
|
||||
Vec3.add(tmpVecB, tmpVecA, tmpDir);
|
||||
}
|
||||
add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], group);
|
||||
Vec3.add(tmpVecA, tmpVecA, tmpDir);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,4 +116,4 @@ function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
|
||||
ib[io] = o; ib[io + 1] = o + 1; ib[io + 2] = o + 2;
|
||||
ib[io + 3] = o + 1; ib[io + 4] = o + 3; ib[io + 5] = o + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 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 { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
|
||||
@@ -97,26 +98,27 @@ export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
}
|
||||
|
||||
export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: BasicCylinderProps) {
|
||||
const s = Math.floor(segmentCount / 2);
|
||||
const step = 1 / segmentCount;
|
||||
|
||||
// automatically adjust length so links/bonds that are rendered as two half cylinders
|
||||
// have evenly spaced dashed cylinders
|
||||
if (lengthScale < 1) {
|
||||
const bias = lengthScale / 2 / segmentCount;
|
||||
lengthScale += segmentCount % 2 === 1 ? bias : -bias;
|
||||
}
|
||||
|
||||
export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, stubCap: boolean, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
const cylinder = getCylinder(props);
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
const isOdd = segmentCount % 2 !== 0;
|
||||
const s = Math.floor((segmentCount + 1) / 2);
|
||||
let step = d / (segmentCount + 0.5);
|
||||
|
||||
let cylinder = getCylinder(props);
|
||||
Vec3.setMagnitude(tmpCylinderDir, Vec3.sub(tmpCylinderDir, end, start), step);
|
||||
Vec3.copy(tmpCylinderStart, start);
|
||||
for (let j = 0; j < s; ++j) {
|
||||
const f = step * (j * 2 + 1);
|
||||
Vec3.setMagnitude(tmpCylinderDir, tmpCylinderDir, d * f);
|
||||
Vec3.add(tmpCylinderStart, start, tmpCylinderDir);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d * step, false);
|
||||
Vec3.add(tmpCylinderStart, tmpCylinderStart, tmpCylinderDir);
|
||||
if (isOdd && j === s - 1) {
|
||||
if (!stubCap && props.topCap) {
|
||||
props.topCap = false;
|
||||
cylinder = getCylinder(props);
|
||||
}
|
||||
step /= 2;
|
||||
}
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, step, false);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
|
||||
Vec3.add(tmpCylinderStart, tmpCylinderStart, tmpCylinderDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +627,7 @@ export namespace Mesh {
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: 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']), BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
@@ -706,7 +706,7 @@ export namespace Mesh {
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
@@ -727,7 +727,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.dXrayShaded, props.xrayShaded);
|
||||
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);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,21 +7,8 @@
|
||||
import { ChunkedArray } from '../../../mol-data/util';
|
||||
import { Spheres } from './spheres';
|
||||
|
||||
const quadMapping = new Float32Array([
|
||||
-1.0, 1.0,
|
||||
-1.0, -1.0,
|
||||
1.0, 1.0,
|
||||
1.0, -1.0
|
||||
]);
|
||||
|
||||
const quadIndices = new Uint16Array([
|
||||
0, 1, 2,
|
||||
1, 3, 2
|
||||
]);
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd2 = ChunkedArray.add2;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
export interface SpheresBuilder {
|
||||
@@ -31,30 +18,18 @@ export interface SpheresBuilder {
|
||||
|
||||
export namespace SpheresBuilder {
|
||||
export function create(initialCount = 2048, chunkSize = 1024, spheres?: Spheres): SpheresBuilder {
|
||||
initialCount *= 4;
|
||||
chunkSize *= 4;
|
||||
const centers = ChunkedArray.create(Float32Array, 3, chunkSize, spheres ? spheres.centerBuffer.ref.value : initialCount);
|
||||
const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, spheres ? spheres.mappingBuffer.ref.value : initialCount);
|
||||
const indices = ChunkedArray.create(Uint32Array, 3, chunkSize / 2, spheres ? spheres.indexBuffer.ref.value : initialCount / 2);
|
||||
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, spheres ? spheres.groupBuffer.ref.value : initialCount);
|
||||
|
||||
return {
|
||||
add: (x: number, y: number, z: number, group: number) => {
|
||||
const offset = centers.elementCount;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
caAdd3(centers, x, y, z);
|
||||
caAdd2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1]);
|
||||
caAdd(groups, group);
|
||||
}
|
||||
caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
caAdd3(centers, x, y, z);
|
||||
caAdd(groups, group);
|
||||
},
|
||||
getSpheres: () => {
|
||||
const cb = ChunkedArray.compact(centers, true) as Float32Array;
|
||||
const mb = ChunkedArray.compact(mappings, true) as Float32Array;
|
||||
const ib = ChunkedArray.compact(indices, true) as Uint32Array;
|
||||
const gb = ChunkedArray.compact(groups, true) as Float32Array;
|
||||
return Spheres.create(cb, mb, ib, gb, centers.elementCount / 4, spheres);
|
||||
return Spheres.create(cb, gb, centers.elementCount, spheres);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { SpheresValues } from '../../../mol-gl/renderable/spheres';
|
||||
import { createColors } from '../color-data';
|
||||
import { createMarkers } from '../marker-data';
|
||||
import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
|
||||
import { TextureImage, calculateInvariantBoundingSphere, calculateTransformBoundingSphere, createTextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { createSizes, getMaxSize } from '../size-data';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
@@ -23,7 +23,7 @@ import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { GroupMapping, createGroupMapping } from '../../util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
|
||||
@@ -35,10 +35,6 @@ export interface Spheres {
|
||||
|
||||
/** Center buffer as array of xyz values wrapped in a value cell */
|
||||
readonly centerBuffer: ValueCell<Float32Array>,
|
||||
/** Mapping buffer as array of xy values wrapped in a value cell */
|
||||
readonly mappingBuffer: ValueCell<Float32Array>,
|
||||
/** Index buffer as array of center index triplets wrapped in a value cell */
|
||||
readonly indexBuffer: ValueCell<Uint32Array>,
|
||||
/** Group buffer as array of group ids for each vertex wrapped in a value cell */
|
||||
readonly groupBuffer: ValueCell<Float32Array>,
|
||||
|
||||
@@ -48,45 +44,51 @@ export interface Spheres {
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
|
||||
shaderData: Spheres.ShaderData
|
||||
}
|
||||
|
||||
export namespace Spheres {
|
||||
export function create(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number, spheres?: Spheres): Spheres {
|
||||
export interface ShaderData {
|
||||
readonly positionGroup: ValueCell<TextureImage<Float32Array>>
|
||||
readonly texDim: ValueCell<Vec2>
|
||||
update(): void
|
||||
}
|
||||
|
||||
export function create(centers: Float32Array, groups: Float32Array, sphereCount: number, spheres?: Spheres): Spheres {
|
||||
return spheres ?
|
||||
update(centers, mappings, indices, groups, sphereCount, spheres) :
|
||||
fromArrays(centers, mappings, indices, groups, sphereCount);
|
||||
update(centers, groups, sphereCount, spheres) :
|
||||
fromArrays(centers, groups, sphereCount);
|
||||
}
|
||||
|
||||
export function createEmpty(spheres?: Spheres): Spheres {
|
||||
const cb = spheres ? spheres.centerBuffer.ref.value : new Float32Array(0);
|
||||
const mb = spheres ? spheres.mappingBuffer.ref.value : new Float32Array(0);
|
||||
const ib = spheres ? spheres.indexBuffer.ref.value : new Uint32Array(0);
|
||||
const gb = spheres ? spheres.groupBuffer.ref.value : new Float32Array(0);
|
||||
return create(cb, mb, ib, gb, 0, spheres);
|
||||
return create(cb, gb, 0, spheres);
|
||||
}
|
||||
|
||||
function hashCode(spheres: Spheres) {
|
||||
return hashFnv32a([
|
||||
spheres.sphereCount,
|
||||
spheres.centerBuffer.ref.version, spheres.mappingBuffer.ref.version,
|
||||
spheres.indexBuffer.ref.version, spheres.groupBuffer.ref.version
|
||||
spheres.centerBuffer.ref.version,
|
||||
spheres.groupBuffer.ref.version
|
||||
]);
|
||||
}
|
||||
|
||||
function fromArrays(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number): Spheres {
|
||||
|
||||
function fromArrays(centers: Float32Array, groups: Float32Array, sphereCount: number): Spheres {
|
||||
const boundingSphere = Sphere3D();
|
||||
let groupMapping: GroupMapping;
|
||||
|
||||
let currentHash = -1;
|
||||
let currentGroup = -1;
|
||||
|
||||
const positionGroup = ValueCell.create(createTextureImage(1, 4, Float32Array));
|
||||
const texDim = ValueCell.create(Vec2.create(0, 0));
|
||||
|
||||
const spheres = {
|
||||
kind: 'spheres' as const,
|
||||
sphereCount,
|
||||
centerBuffer: ValueCell.create(centers),
|
||||
mappingBuffer: ValueCell.create(mappings),
|
||||
indexBuffer: ValueCell.create(indices),
|
||||
groupBuffer: ValueCell.create(groups),
|
||||
get boundingSphere() {
|
||||
const newHash = hashCode(spheres);
|
||||
@@ -107,30 +109,49 @@ export namespace Spheres {
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(spheres);
|
||||
}
|
||||
},
|
||||
shaderData: {
|
||||
positionGroup,
|
||||
texDim,
|
||||
update() {
|
||||
const pgt = createTextureImage(spheres.sphereCount, 4, Float32Array, positionGroup.ref.value.array);
|
||||
setPositionGroup(pgt, spheres.centerBuffer.ref.value, spheres.groupBuffer.ref.value, spheres.sphereCount);
|
||||
ValueCell.update(positionGroup, pgt);
|
||||
ValueCell.update(texDim, Vec2.set(texDim.ref.value, pgt.width, pgt.height));
|
||||
}
|
||||
},
|
||||
};
|
||||
return spheres;
|
||||
}
|
||||
|
||||
function update(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number, spheres: Spheres) {
|
||||
if (sphereCount > spheres.sphereCount) {
|
||||
ValueCell.update(spheres.mappingBuffer, mappings);
|
||||
ValueCell.update(spheres.indexBuffer, indices);
|
||||
}
|
||||
function update(centers: Float32Array, groups: Float32Array, sphereCount: number, spheres: Spheres) {
|
||||
spheres.sphereCount = sphereCount;
|
||||
ValueCell.update(spheres.centerBuffer, centers);
|
||||
ValueCell.update(spheres.groupBuffer, groups);
|
||||
spheres.shaderData.update();
|
||||
return spheres;
|
||||
}
|
||||
|
||||
function setPositionGroup(out: TextureImage<Float32Array>, centers: Float32Array, groups: Float32Array, count: number) {
|
||||
const { array } = out;
|
||||
for (let i = 0; i < count; ++i) {
|
||||
array[i * 4 + 0] = centers[i * 3 + 0];
|
||||
array[i * 4 + 1] = centers[i * 3 + 1];
|
||||
array[i * 4 + 2] = centers[i * 3 + 2];
|
||||
array[i * 4 + 3] = groups[i];
|
||||
}
|
||||
}
|
||||
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: 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']), BaseGeometry.ShadingCategory),
|
||||
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
|
||||
approximate: PD.Boolean(false, { ...BaseGeometry.ShadingCategory, description: 'Faster rendering, but has artifacts.' }),
|
||||
alphaThickness: PD.Numeric(0, { min: 0, max: 20, step: 1 }, { ...BaseGeometry.ShadingCategory, description: 'If not zero, adjusts alpha for radius.' }),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
};
|
||||
@@ -149,7 +170,7 @@ export namespace Spheres {
|
||||
};
|
||||
|
||||
function createPositionIterator(spheres: Spheres, transform: TransformData): LocationIterator {
|
||||
const groupCount = spheres.sphereCount * 4;
|
||||
const groupCount = spheres.sphereCount;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
@@ -163,7 +184,7 @@ export namespace Spheres {
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 4, getLocation);
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function createValues(spheres: Spheres, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): SpheresValues {
|
||||
@@ -180,19 +201,20 @@ export namespace Spheres {
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 4, groupCount, instanceCount };
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 6, groupCount, instanceCount };
|
||||
|
||||
const padding = spheres.boundingSphere.radius ? getMaxSize(size) * props.sizeFactor : 0;
|
||||
const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), spheres.boundingSphere, padding);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount, 0);
|
||||
|
||||
spheres.shaderData.update();
|
||||
|
||||
return {
|
||||
dGeometryType: ValueCell.create('spheres'),
|
||||
|
||||
aPosition: spheres.centerBuffer,
|
||||
aMapping: spheres.mappingBuffer,
|
||||
aGroup: spheres.groupBuffer,
|
||||
elements: spheres.indexBuffer,
|
||||
uTexDim: spheres.shaderData.texDim,
|
||||
tPositionGroup: spheres.shaderData.positionGroup,
|
||||
|
||||
boundingSphere: ValueCell.create(boundingSphere),
|
||||
invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
|
||||
uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
|
||||
@@ -211,11 +233,16 @@ export namespace Spheres {
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor),
|
||||
uDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
dApproximate: ValueCell.create(props.approximate),
|
||||
uAlphaThickness: ValueCell.create(props.alphaThickness),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
|
||||
centerBuffer: spheres.centerBuffer,
|
||||
groupBuffer: spheres.groupBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,9 +257,11 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
|
||||
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);
|
||||
ValueCell.updateIfChanged(values.dApproximate, props.approximate);
|
||||
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export namespace TextureMesh {
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: 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']), BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
@@ -215,7 +215,7 @@ export namespace TextureMesh {
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
@@ -236,7 +236,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.dXrayShaded, props.xrayShaded);
|
||||
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);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
|
||||
@@ -16,7 +16,7 @@ const c = {
|
||||
|
||||
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84FF,
|
||||
MAX_TEXTURE_IMAGE_UNITS_NV: 0x8872
|
||||
};
|
||||
} as const;
|
||||
|
||||
const gl = {
|
||||
ACTIVE_ATTRIBUTES: 35721,
|
||||
@@ -316,7 +316,7 @@ const gl = {
|
||||
VERTEX_SHADER: 35633,
|
||||
VIEWPORT: 2978,
|
||||
ZERO: 0
|
||||
};
|
||||
} as const;
|
||||
type gl = typeof gl
|
||||
|
||||
export function createGl(width: number, height: number, contextAttributes: WebGLContextAttributes): WebGLRenderingContext {
|
||||
@@ -371,66 +371,66 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
|
||||
case 'EXT_blend_minmax': return {
|
||||
MAX_EXT: 0,
|
||||
MIN_EXT: 0
|
||||
} as EXT_blend_minmax;
|
||||
} as unknown as EXT_blend_minmax;
|
||||
case 'EXT_texture_filter_anisotropic': return {
|
||||
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0,
|
||||
TEXTURE_MAX_ANISOTROPY_EXT: 0
|
||||
} as EXT_texture_filter_anisotropic;
|
||||
} as unknown as EXT_texture_filter_anisotropic;
|
||||
case 'EXT_frag_depth': return {} as EXT_frag_depth;
|
||||
case 'EXT_shader_texture_lod': return {} as EXT_shader_texture_lod;
|
||||
case 'EXT_shader_texture_lod': return {} as unknown as EXT_shader_texture_lod;
|
||||
case 'EXT_sRGB': return {
|
||||
FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT: 0,
|
||||
SRGB8_ALPHA8_EXT: 0,
|
||||
SRGB_ALPHA_EXT: 0,
|
||||
SRGB_EXT: 0
|
||||
} as EXT_sRGB;
|
||||
} as unknown as EXT_sRGB;
|
||||
case 'OES_vertex_array_object': return {
|
||||
VERTEX_ARRAY_BINDING_OES: 0,
|
||||
bindVertexArrayOES: function (arrayObject: WebGLVertexArrayObjectOES) { },
|
||||
createVertexArrayOES: function (): WebGLVertexArrayObjectOES { return {}; },
|
||||
deleteVertexArrayOES: function (arrayObject: WebGLVertexArrayObjectOES) { },
|
||||
isVertexArrayOES: function (value: any) { return true; }
|
||||
} as OES_vertex_array_object;
|
||||
} as unknown as OES_vertex_array_object;
|
||||
case 'WEBGL_color_buffer_float': return {
|
||||
FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0,
|
||||
RGB32F_EXT: 0,
|
||||
RGBA32F_EXT: 0,
|
||||
UNSIGNED_NORMALIZED_EXT: 0
|
||||
} as WEBGL_color_buffer_float;
|
||||
} as unknown as WEBGL_color_buffer_float;
|
||||
case 'WEBGL_compressed_texture_astc': return null;
|
||||
case 'WEBGL_compressed_texture_s3tc_srgb': return null;
|
||||
case 'WEBGL_debug_shaders': return {
|
||||
getTranslatedShaderSource(shader: WebGLShader) { return ''; }
|
||||
} as WEBGL_debug_shaders;
|
||||
} as unknown as WEBGL_debug_shaders;
|
||||
case 'WEBGL_draw_buffers': return null;
|
||||
case 'WEBGL_lose_context': return {
|
||||
loseContext: function () { },
|
||||
restoreContext: function () { },
|
||||
} as WEBGL_lose_context;
|
||||
} as unknown as WEBGL_lose_context;
|
||||
case 'WEBGL_depth_texture': return {
|
||||
UNSIGNED_INT_24_8_WEBGL: 0
|
||||
} as WEBGL_depth_texture;
|
||||
} as unknown as WEBGL_depth_texture;
|
||||
case 'WEBGL_debug_renderer_info': return {
|
||||
UNMASKED_RENDERER_WEBGL: 0,
|
||||
UNMASKED_VENDOR_WEBGL: 0
|
||||
} as WEBGL_debug_renderer_info;
|
||||
} as unknown as WEBGL_debug_renderer_info;
|
||||
case 'WEBGL_compressed_texture_s3tc': return null;
|
||||
case 'OES_texture_half_float_linear': return {} as OES_texture_half_float_linear;
|
||||
case 'OES_texture_half_float_linear': return {} as unknown as OES_texture_half_float_linear;
|
||||
case 'OES_texture_half_float': return {
|
||||
HALF_FLOAT_OES: 0
|
||||
} as OES_texture_half_float;
|
||||
case 'OES_texture_float_linear': return {} as OES_texture_float_linear;
|
||||
case 'OES_texture_float': return {} as OES_texture_float;
|
||||
} as unknown as OES_texture_half_float;
|
||||
case 'OES_texture_float_linear': return {} as unknown as OES_texture_float_linear;
|
||||
case 'OES_texture_float': return {} as unknown as OES_texture_float;
|
||||
case 'OES_standard_derivatives': return {
|
||||
FRAGMENT_SHADER_DERIVATIVE_HINT_OES: 0
|
||||
} as OES_standard_derivatives;
|
||||
case 'OES_element_index_uint': return {} as OES_element_index_uint;
|
||||
} as unknown as OES_standard_derivatives;
|
||||
case 'OES_element_index_uint': return {} as unknown as OES_element_index_uint;
|
||||
case 'ANGLE_instanced_arrays': return {
|
||||
drawArraysInstancedANGLE: function (mode: number, first: number, count: number, primcount: number) {},
|
||||
drawElementsInstancedANGLE: function (mode: number, count: number, type: number, offset: number, primcount: number) {},
|
||||
vertexAttribDivisorANGLE: function (index: number, divisor: number) {},
|
||||
VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE: 0
|
||||
} as ANGLE_instanced_arrays;
|
||||
} as unknown as ANGLE_instanced_arrays;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -762,6 +762,7 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
|
||||
isContextLost: function () { return false; },
|
||||
drawingBufferWidth: width,
|
||||
drawingBufferHeight: height,
|
||||
drawingBufferColorSpace: 'srgb',
|
||||
blendColor: function () { },
|
||||
blendEquation: function () { },
|
||||
blendEquationSeparate: function () { },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -17,10 +17,11 @@ export function getGLContext(width: number, height: number) {
|
||||
return createContext(gl);
|
||||
}
|
||||
|
||||
export function tryGetGLContext(width: number, height: number, requiredExtensions?: { fragDepth?: boolean }) {
|
||||
export function tryGetGLContext(width: number, height: number, requiredExtensions?: { fragDepth?: boolean, textureFloat?: boolean }) {
|
||||
try {
|
||||
const ctx = getGLContext(width, height);
|
||||
if (requiredExtensions?.fragDepth && !ctx.extensions.fragDepth) return;
|
||||
if (requiredExtensions?.textureFloat && !ctx.extensions.textureFloat) return;
|
||||
return ctx;
|
||||
} catch (e) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -21,7 +21,7 @@ export function createSpheres() {
|
||||
}
|
||||
|
||||
describe('spheres', () => {
|
||||
const ctx = tryGetGLContext(32, 32, { fragDepth: true });
|
||||
const ctx = tryGetGLContext(32, 32, { fragDepth: true, textureFloat: true });
|
||||
|
||||
(ctx ? it : it.skip)('basic', async () => {
|
||||
const ctx = getGLContext(32, 32);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -25,7 +25,7 @@ export const CylindersSchema = {
|
||||
padding: ValueSpec('number'),
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -40,7 +40,7 @@ export const DirectVolumeSchema = {
|
||||
dAxisOrder: DefineSpec('string', ['012', '021', '102', '120', '201', '210']),
|
||||
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
};
|
||||
export type DirectVolumeSchema = typeof DirectVolumeSchema
|
||||
export type DirectVolumeValues = Values<DirectVolumeSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -22,7 +22,7 @@ export const MeshSchema = {
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
|
||||
@@ -76,13 +76,15 @@ export function AttributeSpec<K extends AttributeKind>(kind: K, itemSize: Attrib
|
||||
return { type: 'attribute', kind, itemSize, divisor };
|
||||
}
|
||||
|
||||
export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, variant?: 'material' | 'buffered' }
|
||||
export function UniformSpec<K extends UniformKind>(kind: K, variant?: 'material' | 'buffered'): UniformSpec<K> {
|
||||
type UniformVariant = 'material' | 'buffered'
|
||||
export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, variant?: UniformVariant }
|
||||
export function UniformSpec<K extends UniformKind>(kind: K, variant?: UniformVariant): UniformSpec<K> {
|
||||
return { type: 'uniform', kind, variant };
|
||||
}
|
||||
|
||||
export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material' }
|
||||
export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material'): TextureSpec<K> {
|
||||
type TextureVariant = 'material'
|
||||
export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: TextureVariant }
|
||||
export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: TextureVariant): TextureSpec<K> {
|
||||
return { type: 'texture', kind, format, dataType, filter, variant };
|
||||
}
|
||||
|
||||
@@ -134,6 +136,7 @@ export const GlobalUniformSchema = {
|
||||
uCameraDir: UniformSpec('v3'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uFog: UniformSpec('b'),
|
||||
uFogNear: UniformSpec('f'),
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
@@ -160,6 +163,7 @@ export const GlobalUniformSchema = {
|
||||
uMarkerAverage: UniformSpec('f'),
|
||||
|
||||
uXrayEdgeFalloff: UniformSpec('f'),
|
||||
uExposure: UniformSpec('f'),
|
||||
|
||||
uRenderMask: UniformSpec('i'),
|
||||
uMarkingDepthTest: UniformSpec('b'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,26 +7,30 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, GraphicsRenderVariant } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec } from './schema';
|
||||
import { SpheresShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
export const SpheresSchema = {
|
||||
...BaseSchema,
|
||||
...SizeSchema,
|
||||
aGroup: AttributeSpec('float32', 1, 0),
|
||||
aPosition: AttributeSpec('float32', 3, 0),
|
||||
aMapping: AttributeSpec('float32', 2, 0),
|
||||
elements: ElementsSpec('uint32'),
|
||||
|
||||
uTexDim: UniformSpec('v2'),
|
||||
tPositionGroup: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
|
||||
|
||||
padding: ValueSpec('number'),
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
dApproximate: DefineSpec('boolean'),
|
||||
uAlphaThickness: UniformSpec('f'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
|
||||
centerBuffer: ValueSpec('float32'),
|
||||
groupBuffer: ValueSpec('float32'),
|
||||
};
|
||||
export type SpheresSchema = typeof SpheresSchema
|
||||
export type SpheresValues = Values<SpheresSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -22,7 +22,7 @@ export const TextureMeshSchema = {
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
|
||||
@@ -49,23 +49,29 @@ const DefaultPrintImageOptions = {
|
||||
pixelated: false,
|
||||
id: 'molstar.debug.image',
|
||||
normalize: false,
|
||||
useCanvas: false,
|
||||
};
|
||||
export type PrintImageOptions = typeof DefaultPrintImageOptions
|
||||
|
||||
export function printTextureImage(textureImage: TextureImage<any>, options: Partial<PrintImageOptions> = {}) {
|
||||
|
||||
const { array, width, height } = textureImage;
|
||||
const itemSize = array.length / (width * height);
|
||||
const data = new Uint8ClampedArray(width * height * 4);
|
||||
const [min, max] = arrayMinMax(array);
|
||||
if (itemSize === 1) {
|
||||
data.fill(255);
|
||||
for (let y = 0; y < height; ++y) {
|
||||
for (let x = 0; x < width; ++x) {
|
||||
data[(y * width + x) * 4 + 3] = array[y * width + x];
|
||||
const i = y * width + x;
|
||||
if (options.normalize) {
|
||||
data[i * 4 + 0] = ((array[i] - min) / (max - min)) * 255;
|
||||
} else {
|
||||
data[i * 4 + 0] = array[i] * 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (itemSize === 4) {
|
||||
if (options.normalize) {
|
||||
const [min, max] = arrayMinMax(array);
|
||||
for (let i = 0, il = width * height * 4; i < il; i += 4) {
|
||||
data[i] = ((array[i] - min) / (max - min)) * 255;
|
||||
data[i + 1] = ((array[i + 1] - min) / (max - min)) * 255;
|
||||
@@ -87,14 +93,6 @@ let tmpContainer: HTMLDivElement;
|
||||
|
||||
export function printImageData(imageData: ImageData, options: Partial<PrintImageOptions> = {}) {
|
||||
const o = { ...DefaultPrintImageOptions, ...options };
|
||||
const canvas = tmpCanvas || document.createElement('canvas');
|
||||
tmpCanvas = canvas;
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = tmpCanvasCtx || canvas.getContext('2d');
|
||||
tmpCanvasCtx = ctx;
|
||||
if (!ctx) throw new Error('Could not create canvas 2d context');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
if (!tmpContainer) {
|
||||
tmpContainer = document.createElement('div');
|
||||
@@ -106,23 +104,52 @@ export function printImageData(imageData: ImageData, options: Partial<PrintImage
|
||||
document.body.appendChild(tmpContainer);
|
||||
}
|
||||
|
||||
canvas.toBlob(imgBlob => {
|
||||
const objectURL = URL.createObjectURL(imgBlob!);
|
||||
const existingImg = document.getElementById(o.id) as HTMLImageElement;
|
||||
const img = existingImg || document.createElement('img');
|
||||
img.id = o.id;
|
||||
img.src = objectURL;
|
||||
img.style.width = imageData.width * o.scale + 'px';
|
||||
img.style.height = imageData.height * o.scale + 'px';
|
||||
if (o.useCanvas) {
|
||||
const existingCanvas = document.getElementById(o.id) as HTMLCanvasElement;
|
||||
const outCanvas = existingCanvas || document.createElement('canvas');
|
||||
outCanvas.width = imageData.width;
|
||||
outCanvas.height = imageData.height;
|
||||
const outCtx = outCanvas.getContext('2d');
|
||||
if (!outCtx) throw new Error('Could not create canvas 2d context');
|
||||
outCtx.putImageData(imageData, 0, 0);
|
||||
outCanvas.id = o.id;
|
||||
outCanvas.style.width = imageData.width * o.scale + 'px';
|
||||
outCanvas.style.height = imageData.height * o.scale + 'px';
|
||||
if (o.pixelated) {
|
||||
// not supported in Firefox and IE
|
||||
img.style.imageRendering = 'pixelated';
|
||||
outCanvas.style.imageRendering = 'pixelated';
|
||||
}
|
||||
img.style.position = 'relative';
|
||||
img.style.border = 'solid grey';
|
||||
img.style.pointerEvents = 'none';
|
||||
if (!existingImg) tmpContainer.appendChild(img);
|
||||
}, 'image/png');
|
||||
outCanvas.style.position = 'relative';
|
||||
outCanvas.style.border = 'solid grey';
|
||||
outCanvas.style.pointerEvents = 'none';
|
||||
if (!existingCanvas) tmpContainer.appendChild(outCanvas);
|
||||
} else {
|
||||
const canvas = tmpCanvas || document.createElement('canvas');
|
||||
tmpCanvas = canvas;
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = tmpCanvasCtx || canvas.getContext('2d');
|
||||
tmpCanvasCtx = ctx;
|
||||
if (!ctx) throw new Error('Could not create canvas 2d context');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
canvas.toBlob(imgBlob => {
|
||||
const objectURL = URL.createObjectURL(imgBlob!);
|
||||
const existingImg = document.getElementById(o.id) as HTMLImageElement;
|
||||
const img = existingImg || document.createElement('img');
|
||||
img.id = o.id;
|
||||
img.src = objectURL;
|
||||
img.style.width = imageData.width * o.scale + 'px';
|
||||
img.style.height = imageData.height * o.scale + 'px';
|
||||
if (o.pixelated) {
|
||||
// not supported in Firefox and IE
|
||||
img.style.imageRendering = 'pixelated';
|
||||
}
|
||||
img.style.position = 'relative';
|
||||
img.style.border = 'solid grey';
|
||||
img.style.pointerEvents = 'none';
|
||||
if (!existingImg) tmpContainer.appendChild(img);
|
||||
}, 'image/png');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -104,6 +104,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 }),
|
||||
exposure: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.01 }),
|
||||
|
||||
light: PD.ObjectList({
|
||||
inclination: PD.Numeric(150, { min: 0, max: 180, step: 1 }),
|
||||
@@ -130,18 +131,19 @@ export type Light = {
|
||||
const tmpDir = Vec3();
|
||||
const tmpColor = Vec3();
|
||||
function getLight(props: RendererProps['light'], light?: Light): Light {
|
||||
const count = props.length;
|
||||
const { direction, color } = light || {
|
||||
direction: (new Array(5 * 3)).fill(0),
|
||||
color: (new Array(5 * 3)).fill(0),
|
||||
direction: (new Array(count * 3)).fill(0),
|
||||
color: (new Array(count * 3)).fill(0),
|
||||
};
|
||||
for (let i = 0, il = props.length; i < il; ++i) {
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
Vec3.directionFromSpherical(tmpDir, degToRad(p.inclination), degToRad(p.azimuth), 1);
|
||||
Vec3.toArray(tmpDir, direction, i * 3);
|
||||
Vec3.scale(tmpColor, Color.toVec3Normalized(tmpColor, p.color), p.intensity);
|
||||
Vec3.toArray(tmpColor, color, i * 3);
|
||||
}
|
||||
return { count: props.length, direction, color };
|
||||
return { count, direction, color };
|
||||
}
|
||||
|
||||
namespace Renderer {
|
||||
@@ -211,6 +213,7 @@ namespace Renderer {
|
||||
uCameraDir: ValueCell.create(cameraDir),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFog: ValueCell.create(true),
|
||||
uFogNear: ValueCell.create(1),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(bgColor),
|
||||
@@ -242,6 +245,7 @@ namespace Renderer {
|
||||
uMarkerAverage: ValueCell.create(0),
|
||||
|
||||
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
|
||||
uExposure: ValueCell.create(p.exposure),
|
||||
};
|
||||
const globalUniformList = Object.entries(globalUniforms);
|
||||
|
||||
@@ -422,7 +426,8 @@ namespace Renderer {
|
||||
const { renderables } = group;
|
||||
for (let i = 0, il = renderables.length; i < il; ++i) {
|
||||
const r = renderables[i];
|
||||
if (r.state.opaque && r.values.transparencyAverage.ref.value !== 1 && !r.values.dXrayShaded?.ref.value) {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if (r.state.opaque && r.values.transparencyAverage.ref.value !== 1 && !xrayShaded) {
|
||||
renderObject(r, 'depth', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -440,7 +445,8 @@ namespace Renderer {
|
||||
const { renderables } = group;
|
||||
for (let i = 0, il = renderables.length; i < il; ++i) {
|
||||
const r = renderables[i];
|
||||
if (!r.state.opaque || r.values.transparencyAverage.ref.value > 0 || r.values.dXrayShaded?.ref.value) {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if (!r.state.opaque || r.values.transparencyAverage.ref.value > 0 || xrayShaded) {
|
||||
renderObject(r, 'depth', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -460,7 +466,8 @@ namespace Renderer {
|
||||
for (let i = 0, il = renderables.length; i < il; ++i) {
|
||||
const r = renderables[i];
|
||||
|
||||
if (r.values.markerAverage.ref.value !== 1) {
|
||||
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
|
||||
if (alpha !== 0 && r.values.markerAverage.ref.value !== 1) {
|
||||
renderObject(renderables[i], 'marking', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -589,7 +596,8 @@ namespace Renderer {
|
||||
// TODO: simplify, handle in renderable.state???
|
||||
// uAlpha is updated in "render" so we need to recompute it here
|
||||
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
|
||||
if ((alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dGeometryType.ref.value !== 'directVolume' && r.values.dPointStyle?.ref.value !== 'fuzzy' && !r.values.dXrayShaded?.ref.value) || r.values.dTransparentBackfaces?.ref.value === 'opaque') {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if ((alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dGeometryType.ref.value !== 'directVolume' && r.values.dPointStyle?.ref.value !== 'fuzzy' && !xrayShaded) || r.values.dTransparentBackfaces?.ref.value === 'opaque') {
|
||||
renderObject(r, 'colorWboit', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -607,7 +615,8 @@ namespace Renderer {
|
||||
// TODO: simplify, handle in renderable.state???
|
||||
// uAlpha is updated in "render" so we need to recompute it here
|
||||
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
|
||||
if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if ((alpha < 1 && alpha !== 0) || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || xrayShaded) {
|
||||
renderObject(r, 'colorWboit', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -629,7 +638,8 @@ namespace Renderer {
|
||||
// TODO: simplify, handle in renderable.state???
|
||||
// uAlpha is updated in "render" so we need to recompute it here
|
||||
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
|
||||
if ((alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dPointStyle?.ref.value !== 'fuzzy' && !r.values.dXrayShaded?.ref.value) || r.values.dTransparentBackfaces?.ref.value === 'opaque') {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if ((alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dPointStyle?.ref.value !== 'fuzzy' && !xrayShaded) || r.values.dTransparentBackfaces?.ref.value === 'opaque') {
|
||||
renderObject(r, 'colorDpoit', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -655,7 +665,8 @@ namespace Renderer {
|
||||
// TODO: simplify, handle in renderable.state???
|
||||
// uAlpha is updated in "render" so we need to recompute it here
|
||||
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
|
||||
if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
|
||||
const xrayShaded = r.values.dXrayShaded?.ref.value === 'on' || r.values.dXrayShaded?.ref.value === 'inverted';
|
||||
if ((alpha < 1 && alpha !== 0) || r.values.transparencyAverage.ref.value > 0 || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || xrayShaded) {
|
||||
renderObject(r, 'colorDpoit', Flag.None);
|
||||
}
|
||||
}
|
||||
@@ -787,6 +798,10 @@ namespace Renderer {
|
||||
p.xrayEdgeFalloff = props.xrayEdgeFalloff;
|
||||
ValueCell.update(globalUniforms.uXrayEdgeFalloff, p.xrayEdgeFalloff);
|
||||
}
|
||||
if (props.exposure !== undefined && props.exposure !== p.exposure) {
|
||||
p.exposure = props.exposure;
|
||||
ValueCell.update(globalUniforms.uExposure, p.exposure);
|
||||
}
|
||||
|
||||
if (props.light !== undefined && !deepEqual(props.light, p.light)) {
|
||||
p.light = props.light;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { WebGLContext } from './webgl/context';
|
||||
import { GraphicsRenderObject, createRenderable } from './render-object';
|
||||
import { Object3D } from './object3d';
|
||||
import { Sphere3D } from '../mol-math/geometry';
|
||||
import { Sphere3D } from '../mol-math/geometry/primitives/sphere3d';
|
||||
import { CommitQueue } from './commit-queue';
|
||||
import { now } from '../mol-util/now';
|
||||
import { arraySetRemove } from '../mol-util/array';
|
||||
@@ -129,10 +129,8 @@ namespace Scene {
|
||||
renderableMap.set(o, renderable);
|
||||
boundingSphereDirty = true;
|
||||
boundingSphereVisibleDirty = true;
|
||||
return renderable;
|
||||
} else {
|
||||
console.warn(`RenderObject with id '${o.id}' already present`);
|
||||
return renderableMap.get(o)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,6 +8,7 @@ import { ValueCell } from '../mol-util';
|
||||
import { idFactory } from '../mol-util/id-factory';
|
||||
import { WebGLExtensions } from './webgl/extensions';
|
||||
import { isWebGL2, GLRenderingContext } from './webgl/compat';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
|
||||
export type DefineKind = 'boolean' | 'string' | 'number'
|
||||
export type DefineType = boolean | string
|
||||
@@ -20,6 +21,8 @@ export interface ShaderExtensions {
|
||||
readonly fragDepth?: ShaderExtensionsValue
|
||||
readonly drawBuffers?: ShaderExtensionsValue
|
||||
readonly shaderTextureLod?: ShaderExtensionsValue
|
||||
readonly clipCullDistance?: ShaderExtensionsValue
|
||||
readonly conservativeDepth?: ShaderExtensionsValue
|
||||
}
|
||||
|
||||
type FragOutTypes = { [k in number]: 'vec4' | 'ivec4' }
|
||||
@@ -127,7 +130,7 @@ function unrollLoops(str: string) {
|
||||
return str.replace(reUnrollLoop, loopReplacer);
|
||||
}
|
||||
|
||||
function loopReplacer(match: string, start: string, end: string, snippet: string) {
|
||||
function loopReplacer(_match: string, start: string, end: string, snippet: string) {
|
||||
let out = '';
|
||||
for (let i = parseInt(start); i < parseInt(end); ++i) {
|
||||
out += snippet
|
||||
@@ -161,9 +164,10 @@ function ignoreDefine(name: string, variant: string, defines: ShaderDefines): bo
|
||||
} else {
|
||||
return [
|
||||
'dColorType', 'dUsePalette',
|
||||
'dLightCount',
|
||||
'dLightCount', 'dXrayShaded',
|
||||
'dOverpaintType', 'dOverpaint',
|
||||
'dSubstanceType', 'dSubstance',
|
||||
'dColorMarker',
|
||||
].includes(name);
|
||||
}
|
||||
return false;
|
||||
@@ -204,7 +208,6 @@ export const DirectVolumeShaderCode = ShaderCode('direct-volume', directVolume_v
|
||||
|
||||
import { image_vert } from './shader/image.vert';
|
||||
import { image_frag } from './shader/image.frag';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
export const ImageShaderCode = ShaderCode('image', image_vert, image_frag, { drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
|
||||
|
||||
//
|
||||
@@ -313,6 +316,22 @@ function getGlsl300VertPrefix(extensions: WebGLExtensions, shaderExtensions: Sha
|
||||
prefix.push('#define requiredDrawBuffers');
|
||||
}
|
||||
}
|
||||
if (shaderExtensions.clipCullDistance) {
|
||||
if (extensions.clipCullDistance) {
|
||||
prefix.push('#extension GL_ANGLE_clip_cull_distance : enable');
|
||||
prefix.push('#define enabledClipCullDistance');
|
||||
} else if (shaderExtensions.clipCullDistance === 'required') {
|
||||
throw new Error(`required 'GL_ANGLE_clip_cull_distance' extension not available`);
|
||||
}
|
||||
}
|
||||
if (shaderExtensions.conservativeDepth) {
|
||||
if (extensions.conservativeDepth) {
|
||||
prefix.push('#extension GL_EXT_conservative_depth : enable');
|
||||
prefix.push('#define enabledConservativeDepth');
|
||||
} else if (shaderExtensions.conservativeDepth === 'required') {
|
||||
throw new Error(`required 'GL_EXT_conservative_depth' extension not available`);
|
||||
}
|
||||
}
|
||||
if (extensions.noNonInstancedActiveAttribs) {
|
||||
prefix.push('#define noNonInstancedActiveAttribs');
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ precision mediump sampler2D;
|
||||
uniform sampler2D tImage;
|
||||
uniform vec2 uImageScale;
|
||||
uniform vec2 uImageOffset;
|
||||
uniform float uBlur;
|
||||
uniform float uOpacity;
|
||||
uniform float uSaturation;
|
||||
uniform float uLightness;
|
||||
@@ -64,7 +65,11 @@ void main() {
|
||||
} else {
|
||||
coords = (gl_FragCoord.xy / uImageScale) + uImageOffset;
|
||||
}
|
||||
gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
|
||||
#ifdef enabledShaderTextureLod
|
||||
gl_FragColor = texture2DLodEXT(tImage, vec2(coords.x, 1.0 - coords.y), uBlur * 8.0);
|
||||
#else
|
||||
gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
|
||||
#endif
|
||||
gl_FragColor.a = uOpacity;
|
||||
gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
|
||||
#elif defined(dVariant_horizontalGradient)
|
||||
|
||||
145
src/mol-gl/shader/cas.frag.ts
Normal file
145
src/mol-gl/shader/cas.frag.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
export const cas_frag = `
|
||||
precision mediump float;
|
||||
precision mediump sampler2D;
|
||||
|
||||
uniform sampler2D tColor;
|
||||
uniform vec2 uTexSizeInv;
|
||||
|
||||
uniform float uSharpness;
|
||||
|
||||
// adapted from https://www.shadertoy.com/view/stXSWB
|
||||
|
||||
/*
|
||||
* FidelityFX Super Resolution scales up a low resolution
|
||||
* image, while adding fine detail.
|
||||
*
|
||||
* MIT Open License
|
||||
*
|
||||
* https://gpuopen.com/fsr
|
||||
*
|
||||
* Left: FSR processed
|
||||
* Right: Original texture, bilinear interpolation
|
||||
*
|
||||
* Mouse at top: Sharpness 0 stops (maximum)
|
||||
* Mouse at bottom: Sharpness 2 stops (minimum)
|
||||
*
|
||||
* It works in two passes-
|
||||
* EASU upsamples the image with a clamped Lanczos kernel.
|
||||
* RCAS sharpens the image at the target resolution.
|
||||
*
|
||||
* I needed to make a few changes to improve readability and
|
||||
* WebGL compatibility in an algorithm I don't fully understand.
|
||||
* Expect bugs.
|
||||
*
|
||||
* Shader not currently running for WebGL1 targets (eg. mobile Safari)
|
||||
*
|
||||
* There is kind of no point to using FSR in Shadertoy, as it renders buffers
|
||||
* at full target resolution. But this might be useful for WebGL based demos
|
||||
* running smaller-than-target render buffers.
|
||||
*
|
||||
* For sharpening with a full resolution render buffer,
|
||||
* FidelityFX CAS is a better option.
|
||||
* https://www.shadertoy.com/view/ftsXzM
|
||||
*
|
||||
* For readability and compatibility, these optimisations have been removed:
|
||||
* * Fast approximate inverse and inversesqrt
|
||||
* * textureGather fetches (not WebGL compatible)
|
||||
* * Multiplying by reciprocal instead of division
|
||||
*
|
||||
* Apologies to AMD for the numerous slowdowns and errors I have introduced.
|
||||
*
|
||||
*/
|
||||
|
||||
/***** RCAS *****/
|
||||
#define FSR_RCAS_LIMIT (0.25-(1.0/16.0))
|
||||
|
||||
// Input callback prototypes that need to be implemented by calling shader
|
||||
vec4 FsrRcasLoadF(vec2 p);
|
||||
//------------------------------------------------------------------------------------------------------------------------------
|
||||
void FsrRcasCon(
|
||||
out float con,
|
||||
// The scale is {0.0 := maximum, to N>0, where N is the number of stops (halving) of the reduction of sharpness}.
|
||||
float sharpness
|
||||
) {
|
||||
// Transform from stops to linear value.
|
||||
con = exp2(-sharpness);
|
||||
}
|
||||
|
||||
vec3 FsrRcasF(
|
||||
vec2 ip, // Integer pixel position in output.
|
||||
float con
|
||||
) {
|
||||
// Constant generated by RcasSetup().
|
||||
// Algorithm uses minimal 3x3 pixel neighborhood.
|
||||
// b
|
||||
// d e f
|
||||
// h
|
||||
vec2 sp = vec2(ip);
|
||||
vec3 b = FsrRcasLoadF(sp + vec2( 0,-1)).rgb;
|
||||
vec3 d = FsrRcasLoadF(sp + vec2(-1, 0)).rgb;
|
||||
vec3 e = FsrRcasLoadF(sp).rgb;
|
||||
vec3 f = FsrRcasLoadF(sp + vec2( 1, 0)).rgb;
|
||||
vec3 h = FsrRcasLoadF(sp + vec2( 0, 1)).rgb;
|
||||
|
||||
// Luma times 2.
|
||||
float bL = b.g + .5 * (b.b + b.r);
|
||||
float dL = d.g + .5 * (d.b + d.r);
|
||||
float eL = e.g + .5 * (e.b + e.r);
|
||||
float fL = f.g + .5 * (f.b + f.r);
|
||||
float hL = h.g + .5 * (h.b + h.r);
|
||||
|
||||
// Noise detection.
|
||||
#ifdef dDenoise
|
||||
float nz = .25 * (bL + dL + fL + hL) - eL;
|
||||
nz=clamp(
|
||||
abs(nz)
|
||||
/(
|
||||
max(max(bL,dL),max(eL,max(fL,hL)))
|
||||
-min(min(bL,dL),min(eL,min(fL,hL)))
|
||||
),
|
||||
0., 1.
|
||||
);
|
||||
nz=1.-.5*nz;
|
||||
#endif
|
||||
|
||||
// Min and max of ring.
|
||||
vec3 mn4 = min(b, min(f, h));
|
||||
vec3 mx4 = max(b, max(f, h));
|
||||
|
||||
// Immediate constants for peak range.
|
||||
vec2 peakC = vec2(1., -4.);
|
||||
|
||||
// Limiters, these need to be high precision RCPs.
|
||||
vec3 hitMin = mn4 / (4. * mx4);
|
||||
vec3 hitMax = (peakC.x - mx4) / (4.* mn4 + peakC.y);
|
||||
vec3 lobeRGB = max(-hitMin, hitMax);
|
||||
float lobe = max(
|
||||
-FSR_RCAS_LIMIT,
|
||||
min(max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)), 0.)
|
||||
)*con;
|
||||
|
||||
// Apply noise removal.
|
||||
#ifdef dDenoise
|
||||
lobe *= nz;
|
||||
#endif
|
||||
|
||||
// Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes.
|
||||
return (lobe * (b + d + h + f) + e) / (4. * lobe + 1.);
|
||||
}
|
||||
|
||||
|
||||
vec4 FsrRcasLoadF(vec2 p) {
|
||||
return texture2D(tColor, p * uTexSizeInv);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Set up constants
|
||||
float con;
|
||||
FsrRcasCon(con, uSharpness);
|
||||
|
||||
// Perform RCAS pass
|
||||
vec3 col = FsrRcasF(gl_FragCoord.xy, con);
|
||||
|
||||
gl_FragColor = vec4(col, FsrRcasLoadF(gl_FragCoord.xy).a);
|
||||
}
|
||||
`;
|
||||
@@ -1,30 +1,32 @@
|
||||
export const apply_fog = `
|
||||
float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
float preFogAlpha = gl_FragColor.a;
|
||||
if (!uTransparentBackground) {
|
||||
if (gl_FragColor.a < 1.0) {
|
||||
// transparent objects are blended with background color
|
||||
gl_FragColor.a = fogAlpha;
|
||||
} else {
|
||||
// mix opaque objects with background color
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
}
|
||||
} else {
|
||||
#if defined(dRenderVariant_colorDpoit)
|
||||
if (uFog) {
|
||||
float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
if (!uTransparentBackground) {
|
||||
if (gl_FragColor.a < 1.0) {
|
||||
// transparent objects are blended with background color
|
||||
gl_FragColor.a = fogAlpha;
|
||||
} else {
|
||||
// opaque objects need to be pre-multiplied alpha
|
||||
// mix opaque objects with background color
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
}
|
||||
} else {
|
||||
#if defined(dRenderVariant_colorDpoit)
|
||||
if (gl_FragColor.a < 1.0) {
|
||||
// transparent objects are blended with background color
|
||||
gl_FragColor.a = fogAlpha;
|
||||
} else {
|
||||
// opaque objects need to be pre-multiplied alpha
|
||||
gl_FragColor.rgb *= fogAlpha;
|
||||
gl_FragColor.a = fogAlpha;
|
||||
}
|
||||
#else
|
||||
// pre-multiplied alpha expected for transparent background
|
||||
gl_FragColor.rgb *= fogAlpha;
|
||||
gl_FragColor.a = fogAlpha;
|
||||
}
|
||||
#else
|
||||
// pre-multiplied alpha expected for transparent background
|
||||
gl_FragColor.rgb *= fogAlpha;
|
||||
gl_FragColor.a = fogAlpha;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*
|
||||
@@ -9,6 +9,13 @@
|
||||
|
||||
export const apply_light_color = `
|
||||
#ifdef dIgnoreLight
|
||||
#ifdef bumpEnabled
|
||||
if (uBumpFrequency > 0.0 && uBumpAmplitude > 0.0 && bumpiness > 0.0) {
|
||||
material.rgb += fbm(vModelPosition * uBumpFrequency) * (uBumpAmplitude * bumpiness) / uBumpFrequency;
|
||||
material.rgb -= bumpiness / (2.0 * uBumpFrequency);
|
||||
}
|
||||
#endif
|
||||
|
||||
gl_FragColor = material;
|
||||
#else
|
||||
#ifdef bumpEnabled
|
||||
@@ -62,7 +69,11 @@ export const apply_light_color = `
|
||||
gl_FragColor = vec4(outgoingLight, color.a);
|
||||
#endif
|
||||
|
||||
#ifdef dXrayShaded
|
||||
#if defined(dXrayShaded_on)
|
||||
gl_FragColor.a *= 1.0 - pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
|
||||
#elif defined(dXrayShaded_inverted)
|
||||
gl_FragColor.a *= pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
|
||||
#endif
|
||||
|
||||
gl_FragColor.rgb *= uExposure;
|
||||
`;
|
||||
@@ -65,7 +65,7 @@ export const common_clip = `
|
||||
|
||||
#if __VERSION__ == 100
|
||||
// 8-bit
|
||||
int bitwiseAnd(const in int a, const in int b) {
|
||||
int bitwiseAnd(in int a, in int b) {
|
||||
int d = 128;
|
||||
int result = 0;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
|
||||
@@ -57,6 +57,7 @@ uniform float uNear;
|
||||
uniform float uFar;
|
||||
uniform float uIsOrtho;
|
||||
|
||||
uniform bool uFog;
|
||||
uniform float uFogNear;
|
||||
uniform float uFogFar;
|
||||
uniform vec3 uFogColor;
|
||||
@@ -72,6 +73,7 @@ uniform vec3 uInteriorColor;
|
||||
bool interior;
|
||||
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform mat4 uProjection;
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ export const common = `
|
||||
#define dNeedsMarker
|
||||
#endif
|
||||
|
||||
#if defined(dXrayShaded_on) || defined(dXrayShaded_inverted)
|
||||
#define dXrayShaded
|
||||
#endif
|
||||
|
||||
#define MaskAll 0
|
||||
#define MaskOpaque 1
|
||||
#define MaskTransparent 2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
@@ -11,9 +11,8 @@ export const dpoit_write = `
|
||||
discard;
|
||||
}
|
||||
} else if (uRenderMask == MaskTransparent) {
|
||||
// the 'fragmentDepth > 0.99' check is to handle precision issues with packed depth
|
||||
vec2 coords = gl_FragCoord.xy / uDrawingBufferSize;
|
||||
if (preFogAlpha != 1.0 && (fragmentDepth < getDepth(coords) || fragmentDepth > 0.99)) {
|
||||
if (preFogAlpha != 1.0 && fragmentDepth < getDepth(coords)) {
|
||||
#ifdef dTransparentBackfaces_off
|
||||
if (interior) discard;
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 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>
|
||||
@@ -12,8 +12,7 @@ export const wboit_write = `
|
||||
discard;
|
||||
}
|
||||
} else if (uRenderMask == MaskTransparent) {
|
||||
// the 'fragmentDepth > 0.99' check is to handle precision issues with packed depth
|
||||
if (preFogAlpha != 1.0 && (fragmentDepth < getDepth(gl_FragCoord.xy / uDrawingBufferSize) || fragmentDepth > 0.99)) {
|
||||
if (preFogAlpha != 1.0 && fragmentDepth < getDepth(gl_FragCoord.xy / uDrawingBufferSize)) {
|
||||
#ifdef dTransparentBackfaces_off
|
||||
if (interior) discard;
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -71,8 +71,10 @@ void main() {
|
||||
vViewPosition = mvPosition.xyz;
|
||||
gl_Position = uProjection * mvPosition;
|
||||
|
||||
mvPosition.z -= 2.0 * (length(vEnd - vStart) + vSize); // avoid clipping
|
||||
gl_Position.z = (uProjection * mvPosition).z;
|
||||
if (gl_Position.z < -gl_Position.w) {
|
||||
mvPosition.z -= 2.0 * (length(vEnd - vStart) + vSize); // avoid clipping
|
||||
gl_Position.z = (uProjection * mvPosition).z;
|
||||
}
|
||||
|
||||
#include clip_instance
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ uniform int uGroupCount;
|
||||
uniform float uMetalness;
|
||||
uniform float uRoughness;
|
||||
|
||||
uniform bool uFog;
|
||||
uniform float uFogNear;
|
||||
uniform float uFogFar;
|
||||
uniform vec3 uFogColor;
|
||||
@@ -75,6 +76,7 @@ uniform vec3 uFogColor;
|
||||
uniform float uAlpha;
|
||||
uniform bool uTransparentBackground;
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform int uRenderMask;
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ uniform vec2 uTexSize;
|
||||
|
||||
uniform float uNear;
|
||||
uniform float uFar;
|
||||
uniform mat4 uInvProjection;
|
||||
|
||||
uniform float uMaxPossibleViewZDiff;
|
||||
uniform float uOutlineThreshold;
|
||||
|
||||
#include common
|
||||
|
||||
@@ -49,17 +50,25 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
|
||||
vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
|
||||
return distance(viewPos0, viewPos1);
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
|
||||
float backgroundViewZ = 2.0 * uFar;
|
||||
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
|
||||
float selfDepthOpaque = getDepthOpaque(coords);
|
||||
float selfViewZOpaque = isBackground(selfDepthOpaque) ? backgroundViewZ : getViewZ(selfDepthOpaque);
|
||||
float pixelSizeOpaque = getPixelSize(coords, selfDepthOpaque) * uOutlineThreshold;
|
||||
|
||||
float selfDepthTransparent = getDepthTransparent(coords);
|
||||
float selfViewZTransparent = isBackground(selfDepthTransparent) ? backgroundViewZ : getViewZ(selfDepthTransparent);
|
||||
float pixelSizeTransparent = getPixelSize(coords, selfDepthTransparent) * uOutlineThreshold;
|
||||
|
||||
float outline = 1.0;
|
||||
float bestDepth = 1.0;
|
||||
@@ -73,14 +82,14 @@ void main(void) {
|
||||
float sampleDepthTransparent = getDepthTransparent(sampleCoords);
|
||||
|
||||
float sampleViewZOpaque = isBackground(sampleDepthOpaque) ? backgroundViewZ : getViewZ(sampleDepthOpaque);
|
||||
if (abs(selfViewZOpaque - sampleViewZOpaque) > uMaxPossibleViewZDiff && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
|
||||
if (abs(selfViewZOpaque - sampleViewZOpaque) > pixelSizeOpaque && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
|
||||
outline = 0.0;
|
||||
bestDepth = sampleDepthOpaque;
|
||||
}
|
||||
|
||||
if (sampleDepthTransparent < sampleDepthOpaque) {
|
||||
float sampleViewZTransparent = isBackground(sampleDepthTransparent) ? backgroundViewZ : getViewZ(sampleDepthTransparent);
|
||||
if (abs(selfViewZTransparent - sampleViewZTransparent) > uMaxPossibleViewZDiff && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
|
||||
if (abs(selfViewZTransparent - sampleViewZTransparent) > pixelSizeTransparent && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
|
||||
outline = 0.0;
|
||||
bestDepth = sampleDepthTransparent;
|
||||
transparentFlag = 1.0;
|
||||
|
||||
@@ -24,16 +24,10 @@ uniform float uFogNear;
|
||||
uniform float uFogFar;
|
||||
uniform vec3 uFogColor;
|
||||
uniform vec3 uOutlineColor;
|
||||
uniform vec3 uOcclusionColor;
|
||||
uniform bool uTransparentBackground;
|
||||
|
||||
uniform vec2 uOcclusionOffset;
|
||||
|
||||
uniform float uMaxPossibleViewZDiff;
|
||||
uniform mat4 uInvProjection;
|
||||
|
||||
const float outlineDistanceFactor = 5.0;
|
||||
const vec3 occlusionColor = vec3(0.0);
|
||||
|
||||
#include common
|
||||
|
||||
float getViewZ(const in float depth) {
|
||||
@@ -64,21 +58,14 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
|
||||
vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
|
||||
return distance(viewPos0, viewPos1);
|
||||
}
|
||||
|
||||
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
|
||||
float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
|
||||
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 pixelSize = getPixelSize(coords, selfDepth);
|
||||
|
||||
float outline = 1.0;
|
||||
closestTexel = 1.0;
|
||||
@@ -96,7 +83,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
|
||||
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
|
||||
|
||||
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
|
||||
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel && abs(selfViewZ - sampleOutlineViewZ) > uMaxPossibleViewZDiff + (pixelSize * outlineDistanceFactor)) {
|
||||
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
|
||||
outline = 0.0;
|
||||
closestTexel = sampleOutlineDepth;
|
||||
}
|
||||
@@ -130,9 +117,9 @@ void main(void) {
|
||||
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
float occlusionFactor = getSsao(coords + uOcclusionOffset);
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
|
||||
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
|
||||
} else {
|
||||
color.rgb = mix(occlusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
|
||||
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -17,9 +17,9 @@ precision highp int;
|
||||
#include common_clip
|
||||
|
||||
uniform mat4 uInvView;
|
||||
uniform float uAlphaThickness;
|
||||
|
||||
varying float vRadius;
|
||||
varying float vRadiusSq;
|
||||
varying vec3 vPoint;
|
||||
varying vec3 vPointViewPosition;
|
||||
|
||||
@@ -37,7 +37,7 @@ bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal
|
||||
vec3 cameraSphereDir = mix(cameraSpherePos, rayOrigin - cameraSpherePos, uIsOrtho);
|
||||
|
||||
float B = dot(rayDirection, cameraSphereDir);
|
||||
float det = B * B + vRadiusSq - dot(cameraSphereDir, cameraSphereDir);
|
||||
float det = B * B + vRadius * vRadius - dot(cameraSphereDir, cameraSphereDir);
|
||||
|
||||
if (det < 0.0) return false;
|
||||
|
||||
@@ -83,21 +83,34 @@ bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal
|
||||
}
|
||||
|
||||
void main(void){
|
||||
vec3 modelPos;
|
||||
vec3 cameraPos;
|
||||
vec3 cameraNormal;
|
||||
float fragmentDepth;
|
||||
bool clipped = false;
|
||||
bool hit = SphereImpostor(modelPos, cameraPos, cameraNormal, interior, fragmentDepth);
|
||||
if (!hit) discard;
|
||||
|
||||
if (fragmentDepth < 0.0) discard;
|
||||
if (fragmentDepth > 1.0) discard;
|
||||
#ifdef dApproximate
|
||||
vec3 pointDir = -vPointViewPosition - vPoint;
|
||||
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);
|
||||
#endif
|
||||
interior = false;
|
||||
#else
|
||||
vec3 modelPos;
|
||||
vec3 cameraPos;
|
||||
bool hit = SphereImpostor(modelPos, cameraPos, cameraNormal, interior, fragmentDepth);
|
||||
if (!hit) discard;
|
||||
|
||||
vec3 vViewPosition = cameraPos;
|
||||
vec3 vModelPosition = modelPos;
|
||||
if (fragmentDepth < 0.0) discard;
|
||||
if (fragmentDepth > 1.0) discard;
|
||||
|
||||
gl_FragDepthEXT = fragmentDepth;
|
||||
gl_FragDepthEXT = fragmentDepth;
|
||||
|
||||
vec3 vModelPosition = modelPos;
|
||||
vec3 vViewPosition = cameraPos;
|
||||
#endif
|
||||
|
||||
#include clip_pixel
|
||||
#include assign_material_color
|
||||
@@ -120,6 +133,10 @@ void main(void){
|
||||
vec3 normal = -cameraNormal;
|
||||
#include apply_light_color
|
||||
|
||||
if (uRenderMask == MaskTransparent && uAlphaThickness > 0.0) {
|
||||
gl_FragColor.a *= min(1.0, vRadius / uAlphaThickness);
|
||||
}
|
||||
|
||||
#include apply_interior_color
|
||||
#include apply_marker_color
|
||||
#include apply_fog
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -17,65 +17,63 @@ precision highp int;
|
||||
|
||||
uniform mat4 uModelView;
|
||||
uniform mat4 uInvProjection;
|
||||
uniform float uIsOrtho;
|
||||
|
||||
uniform vec2 uTexDim;
|
||||
uniform sampler2D tPositionGroup;
|
||||
|
||||
attribute vec3 aPosition;
|
||||
attribute vec2 aMapping;
|
||||
attribute mat4 aTransform;
|
||||
attribute float aInstance;
|
||||
attribute float aGroup;
|
||||
|
||||
varying float vRadius;
|
||||
varying float vRadiusSq;
|
||||
varying vec3 vPoint;
|
||||
varying vec3 vPointViewPosition;
|
||||
|
||||
#include matrix_scale
|
||||
|
||||
const mat4 D = mat4(
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, -1.0
|
||||
);
|
||||
|
||||
/**
|
||||
* Compute point size and center using the technique described in:
|
||||
* "GPU-Based Ray-Casting of Quadratic Surfaces" http://dl.acm.org/citation.cfm?id=2386396
|
||||
* by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.
|
||||
* Bounding rectangle of a clipped, perspective-projected 3D Sphere.
|
||||
* Michael Mara, Morgan McGuire. 2013
|
||||
*
|
||||
* Specialization by Arseny Kapoulkine, MIT License Copyright (c) 2018
|
||||
* https://github.com/zeux/niagara
|
||||
*/
|
||||
void quadraticProjection(const in float radius, const in vec3 position){
|
||||
vec2 xbc, ybc;
|
||||
void sphereProjection(const in vec3 p, const in float r, const in vec2 mapping) {
|
||||
vec3 pr = p * r;
|
||||
float pzr2 = p.z * p.z - r * r;
|
||||
|
||||
mat4 T = mat4(
|
||||
radius, 0.0, 0.0, 0.0,
|
||||
0.0, radius, 0.0, 0.0,
|
||||
0.0, 0.0, radius, 0.0,
|
||||
position.x, position.y, position.z, 1.0
|
||||
);
|
||||
float vx = sqrt(p.x * p.x + pzr2);
|
||||
float minx = ((vx * p.x - pr.z) / (vx * p.z + pr.x)) * uProjection[0][0];
|
||||
float maxx = ((vx * p.x + pr.z) / (vx * p.z - pr.x)) * uProjection[0][0];
|
||||
|
||||
mat4 R = transpose4(uProjection * uModelView * aTransform * T);
|
||||
float A = dot(R[3], D * R[3]);
|
||||
float B = -2.0 * dot(R[0], D * R[3]);
|
||||
float C = dot(R[0], D * R[0]);
|
||||
xbc[0] = (-B - sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
|
||||
xbc[1] = (-B + sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
|
||||
float sx = abs(xbc[0] - xbc[1]) * 0.5;
|
||||
float vy = sqrt(p.y * p.y + pzr2);
|
||||
float miny = ((vy * p.y - pr.z) / (vy * p.z + pr.y)) * uProjection[1][1];
|
||||
float maxy = ((vy * p.y + pr.z) / (vy * p.z - pr.y)) * uProjection[1][1];
|
||||
|
||||
A = dot(R[3], D * R[3]);
|
||||
B = -2.0 * dot(R[1], D * R[3]);
|
||||
C = dot(R[1], D * R[1]);
|
||||
ybc[0] = (-B - sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
|
||||
ybc[1] = (-B + sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
|
||||
float sy = abs(ybc[0] - ybc[1]) * 0.5;
|
||||
|
||||
gl_Position.xy = vec2(0.5 * (xbc.x + xbc.y), 0.5 * (ybc.x + ybc.y));
|
||||
gl_Position.xy -= aMapping * vec2(sx, sy);
|
||||
gl_Position.xy = vec2(maxx + minx, maxy + miny) * -0.5;
|
||||
gl_Position.xy -= mapping * vec2(maxx - minx, maxy - miny) * 0.5;
|
||||
gl_Position.xy *= gl_Position.w;
|
||||
}
|
||||
|
||||
|
||||
void main(void){
|
||||
#include assign_group
|
||||
vec2 mapping = vec2(1.0, 1.0); // vertices 2 and 5
|
||||
#if __VERSION__ == 100
|
||||
int m = imod(VertexID, 6);
|
||||
#else
|
||||
int m = VertexID % 6;
|
||||
#endif
|
||||
if (m == 0) {
|
||||
mapping = vec2(-1.0, 1.0);
|
||||
} else if (m == 1 || m == 3) {
|
||||
mapping = vec2(-1.0, -1.0);
|
||||
} else if (m == 4) {
|
||||
mapping = vec2(1.0, -1.0);
|
||||
}
|
||||
|
||||
vec4 positionGroup = readFromTexture(tPositionGroup, VertexID / 6, uTexDim);
|
||||
vec3 position = positionGroup.rgb;
|
||||
float group = positionGroup.a;
|
||||
|
||||
#include assign_color_varying
|
||||
#include assign_marker_varying
|
||||
#include assign_clipping_varying
|
||||
@@ -83,21 +81,34 @@ void main(void){
|
||||
|
||||
vRadius = size * matrixScale(uModelView);
|
||||
|
||||
vec4 position4 = vec4(aPosition, 1.0);
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
vec4 mvPosition = uModelView * aTransform * position4;
|
||||
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
quadraticProjection(size, aPosition);
|
||||
#ifdef dApproximate
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
mvCorner.xy += mapping * vRadius;
|
||||
gl_Position = uProjection * mvCorner;
|
||||
#else
|
||||
if (uIsOrtho == 1.0) {
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
mvCorner.xy += mapping * vRadius;
|
||||
gl_Position = uProjection * mvCorner;
|
||||
} else {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
sphereProjection(mvPosition.xyz, vRadius, mapping);
|
||||
}
|
||||
#endif
|
||||
|
||||
vRadiusSq = vRadius * vRadius;
|
||||
vec4 vPoint4 = uInvProjection * gl_Position;
|
||||
vPoint = vPoint4.xyz / vPoint4.w;
|
||||
vPointViewPosition = -mvPosition.xyz / mvPosition.w;
|
||||
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
|
||||
mvPosition.z -= 2.0 * vRadius; // avoid clipping
|
||||
gl_Position.z = (uProjection * vec4(mvPosition.xyz, 1.0)).z;
|
||||
if (gl_Position.z < -gl_Position.w) {
|
||||
mvPosition.z -= 2.0 * vRadius; // avoid clipping
|
||||
gl_Position.z = (uProjection * vec4(mvPosition.xyz, 1.0)).z;
|
||||
}
|
||||
|
||||
#include clip_instance
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -19,8 +19,7 @@ uniform float uKernel[dOcclusionKernelSize];
|
||||
uniform float uBlurDirectionX;
|
||||
uniform float uBlurDirectionY;
|
||||
|
||||
uniform float uMaxPossibleViewZDiff;
|
||||
|
||||
uniform mat4 uInvProjection;
|
||||
uniform float uNear;
|
||||
uniform float uFar;
|
||||
|
||||
@@ -42,6 +41,12 @@ bool outsideBounds(const in vec2 p) {
|
||||
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
|
||||
}
|
||||
|
||||
float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
|
||||
vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
|
||||
return distance(viewPos0, viewPos1);
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
|
||||
@@ -60,6 +65,8 @@ void main(void) {
|
||||
}
|
||||
|
||||
float selfViewZ = getViewZ(selfDepth);
|
||||
float pixelSize = getPixelSize(coords, selfDepth);
|
||||
float maxDiffViewZ = pixelSize * 10.0;
|
||||
|
||||
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
|
||||
|
||||
@@ -67,6 +74,8 @@ void main(void) {
|
||||
float kernelSum = 0.0;
|
||||
// only if kernelSize is odd
|
||||
for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
|
||||
if (abs(float(i)) > 1.0 && abs(float(i)) * pixelSize > 0.8) continue;
|
||||
|
||||
vec2 sampleCoords = coords + float(i) * offset;
|
||||
if (outsideBounds(sampleCoords)) {
|
||||
continue;
|
||||
@@ -79,9 +88,9 @@ void main(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (abs(float(i)) > 1.0) { // abs is not defined for int in webgl1
|
||||
if (abs(float(i)) > 1.0) {
|
||||
float sampleViewZ = getViewZ(sampleDepth);
|
||||
if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff) {
|
||||
if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ precision highp sampler2D;
|
||||
#include common
|
||||
|
||||
uniform sampler2D tDepth;
|
||||
uniform sampler2D tDepthHalf;
|
||||
uniform sampler2D tDepthQuarter;
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
@@ -21,7 +23,14 @@ uniform vec3 uSamples[dNSamples];
|
||||
uniform mat4 uProjection;
|
||||
uniform mat4 uInvProjection;
|
||||
|
||||
uniform float uRadius;
|
||||
#ifdef dMultiScale
|
||||
uniform float uLevelRadius[dLevels];
|
||||
uniform float uLevelBias[dLevels];
|
||||
uniform float uNearThreshold;
|
||||
uniform float uFarThreshold;
|
||||
#else
|
||||
uniform float uRadius;
|
||||
#endif
|
||||
uniform float uBias;
|
||||
|
||||
float smootherstep(float edge0, float edge1, float x) {
|
||||
@@ -46,20 +55,38 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
bool outsideBounds(const in vec2 p) {
|
||||
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
|
||||
float getDepth(const in vec2 coords) {
|
||||
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepth, c).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepth, c));
|
||||
#endif
|
||||
}
|
||||
|
||||
float getDepth(const in vec2 coords) {
|
||||
if (outsideBounds(coords)) {
|
||||
return 1.0;
|
||||
} else {
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepth, coords).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepth, coords));
|
||||
#endif
|
||||
}
|
||||
#define dQuarterThreshold 0.1
|
||||
#define dHalfThreshold 0.05
|
||||
|
||||
float getMappedDepth(const in vec2 coords, const in vec2 selfCoords) {
|
||||
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
|
||||
float d = distance(coords, selfCoords);
|
||||
#ifdef depthTextureSupport
|
||||
if (d > dQuarterThreshold) {
|
||||
return texture2D(tDepthQuarter, c).r;
|
||||
} else if (d > dHalfThreshold) {
|
||||
return texture2D(tDepthHalf, c).r;
|
||||
} else {
|
||||
return texture2D(tDepth, c).r;
|
||||
}
|
||||
#else
|
||||
if (d > dQuarterThreshold) {
|
||||
return unpackRGBAToDepth(texture2D(tDepthQuarter, c));
|
||||
} else if (d > dHalfThreshold) {
|
||||
return unpackRGBAToDepth(texture2D(tDepthHalf, c));
|
||||
} else {
|
||||
return unpackRGBAToDepth(texture2D(tDepth, c));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
|
||||
@@ -72,6 +99,12 @@ vec3 normalFromDepth(const in float depth, const in float depth1, const in float
|
||||
return normalize(normal);
|
||||
}
|
||||
|
||||
float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
|
||||
vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
|
||||
return distance(viewPos0, viewPos1);
|
||||
}
|
||||
|
||||
// StarCraft II Ambient Occlusion by [Filion and McNaughton 2008]
|
||||
void main(void) {
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
@@ -81,7 +114,7 @@ void main(void) {
|
||||
vec2 selfPackedDepth = packUnitIntervalToRG(selfDepth);
|
||||
|
||||
if (isBackground(selfDepth)) {
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(0.0), selfPackedDepth);
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), selfPackedDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,24 +128,50 @@ void main(void) {
|
||||
vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
|
||||
|
||||
vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
|
||||
|
||||
vec3 tangent = normalize(randomVec - selfViewNormal * dot(randomVec, selfViewNormal));
|
||||
vec3 bitangent = cross(selfViewNormal, tangent);
|
||||
mat3 TBN = mat3(tangent, bitangent, selfViewNormal);
|
||||
|
||||
float occlusion = 0.0;
|
||||
for(int i = 0; i < dNSamples; i++){
|
||||
vec3 sampleViewPos = TBN * uSamples[i];
|
||||
sampleViewPos = selfViewPos + sampleViewPos * uRadius;
|
||||
#ifdef dMultiScale
|
||||
float pixelSize = getPixelSize(selfCoords, selfDepth);
|
||||
|
||||
vec4 offset = vec4(sampleViewPos, 1.0);
|
||||
offset = uProjection * offset;
|
||||
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
|
||||
for(int l = 0; l < dLevels; l++) {
|
||||
// TODO: smooth transition
|
||||
if (pixelSize * uNearThreshold > uLevelRadius[l]) continue;
|
||||
if (pixelSize * uFarThreshold < uLevelRadius[l]) continue;
|
||||
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, getDepth(offset.xy)), uInvProjection).z;
|
||||
float levelOcclusion = 0.0;
|
||||
for(int i = 0; i < dNSamples; i++) {
|
||||
vec3 sampleViewPos = TBN * uSamples[i];
|
||||
sampleViewPos = selfViewPos + sampleViewPos * uLevelRadius[l];
|
||||
|
||||
occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
|
||||
}
|
||||
vec4 offset = vec4(sampleViewPos, 1.0);
|
||||
offset = uProjection * offset;
|
||||
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
|
||||
|
||||
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
|
||||
levelOcclusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
|
||||
}
|
||||
occlusion = max(occlusion, levelOcclusion);
|
||||
}
|
||||
#else
|
||||
for(int i = 0; i < dNSamples; i++) {
|
||||
vec3 sampleViewPos = TBN * uSamples[i];
|
||||
sampleViewPos = selfViewPos + sampleViewPos * uRadius;
|
||||
|
||||
vec4 offset = vec4(sampleViewPos, 1.0);
|
||||
offset = uProjection * offset;
|
||||
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
|
||||
|
||||
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
|
||||
occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
|
||||
}
|
||||
#endif
|
||||
occlusion = 1.0 - (uBias * occlusion / float(dNSamples));
|
||||
|
||||
vec2 packedOcclusion = packUnitIntervalToRG(clamp(occlusion, 0.01, 1.0));
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface Buffer {
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
function getBuffer(gl: GLRenderingContext) {
|
||||
export function getBuffer(gl: GLRenderingContext) {
|
||||
const buffer = gl.createBuffer();
|
||||
if (buffer === null) {
|
||||
throw new Error('Could not create WebGL buffer');
|
||||
|
||||
@@ -23,8 +23,28 @@ export function isWebGL2(gl: any): gl is WebGL2RenderingContext {
|
||||
* See https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/
|
||||
*/
|
||||
export interface COMPAT_instanced_arrays {
|
||||
/**
|
||||
* Renders primitives from array data like the `drawArrays` method. In addition, it can execute multiple instances of the range of elements.
|
||||
* @param mode the type primitive to render.
|
||||
* @param first the starting index in the array of vector points.
|
||||
* @param count the number of indices to be rendered.
|
||||
* @param primcount the number of instances of the range of elements to execute.
|
||||
*/
|
||||
drawArraysInstanced(mode: number, first: number, count: number, primcount: number): void;
|
||||
/**
|
||||
* Renders primitives from array data like the `drawElements` method. In addition, it can execute multiple instances of a set of elements.
|
||||
* @param mode the type primitive to render.
|
||||
* @param count the number of elements to be rendered.
|
||||
* @param type the type of the values in the element array buffer.
|
||||
* @param offset an offset in the element array buffer. Must be a valid multiple of the size of the given `type`.
|
||||
* @param primcount the number of instances of the set of elements to execute.
|
||||
*/
|
||||
drawElementsInstanced(mode: number, count: number, type: number, offset: number, primcount: number): void;
|
||||
/**
|
||||
* Modifies the rate at which generic vertex attributes advance when rendering multiple instances of primitives with `drawArraysInstanced` and `drawElementsInstanced`
|
||||
* @param index the index of the generic vertex attributes.
|
||||
* @param divisor the number of instances that will pass between updates of the generic attribute.
|
||||
*/
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
readonly VERTEX_ATTRIB_ARRAY_DIVISOR: number;
|
||||
}
|
||||
@@ -109,6 +129,9 @@ export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_arra
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/OES_texture_float/
|
||||
*/
|
||||
export interface COMPAT_texture_float {
|
||||
}
|
||||
|
||||
@@ -116,6 +139,9 @@ export function getTextureFloat(gl: GLRenderingContext): COMPAT_texture_float |
|
||||
return isWebGL2(gl) ? {} : gl.getExtension('OES_texture_float');
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/OES_texture_float_linear/
|
||||
*/
|
||||
export interface COMPAT_texture_float_linear {
|
||||
}
|
||||
|
||||
@@ -123,6 +149,9 @@ export function getTextureFloatLinear(gl: GLRenderingContext): COMPAT_texture_fl
|
||||
return gl.getExtension('OES_texture_float_linear');
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/OES_texture_half_float/
|
||||
*/
|
||||
export interface COMPAT_texture_half_float {
|
||||
readonly HALF_FLOAT: number
|
||||
}
|
||||
@@ -137,6 +166,9 @@ export function getTextureHalfFloat(gl: GLRenderingContext): COMPAT_texture_half
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/OES_texture_half_float_linear/
|
||||
*/
|
||||
export interface COMPAT_texture_half_float_linear {
|
||||
}
|
||||
|
||||
@@ -172,6 +204,9 @@ export function getFragDepth(gl: GLRenderingContext): COMPAT_frag_depth | null {
|
||||
return isWebGL2(gl) ? {} : gl.getExtension('EXT_frag_depth');
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/
|
||||
*/
|
||||
export interface COMPAT_color_buffer_float {
|
||||
readonly RGBA32F: number;
|
||||
}
|
||||
@@ -193,6 +228,9 @@ export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_color_buffer_half_float/
|
||||
*/
|
||||
export interface COMPAT_color_buffer_half_float {
|
||||
readonly RGBA16F: number;
|
||||
}
|
||||
@@ -548,6 +586,123 @@ export function getProvokingVertex(gl: GLRenderingContext): COMPAT_provoking_ver
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/WEBGL_clip_cull_distance/
|
||||
*/
|
||||
export interface COMPAT_clip_cull_distance {
|
||||
readonly MAX_CLIP_DISTANCES: number;
|
||||
readonly MAX_CULL_DISTANCES: number;
|
||||
readonly MAX_COMBINED_CLIP_AND_CULL_DISTANCES: number;
|
||||
|
||||
readonly CLIP_DISTANCE0: number;
|
||||
readonly CLIP_DISTANCE1: number;
|
||||
readonly CLIP_DISTANCE2: number;
|
||||
readonly CLIP_DISTANCE3: number;
|
||||
readonly CLIP_DISTANCE4: number;
|
||||
readonly CLIP_DISTANCE5: number;
|
||||
readonly CLIP_DISTANCE6: number;
|
||||
readonly CLIP_DISTANCE7: number;
|
||||
}
|
||||
|
||||
export function getClipCullDistance(gl: GLRenderingContext): COMPAT_clip_cull_distance | null {
|
||||
if (isWebGL2(gl)) {
|
||||
const ext = gl.getExtension('WEBGL_clip_cull_distance');
|
||||
if (ext) {
|
||||
return {
|
||||
MAX_CLIP_DISTANCES: ext.MAX_CLIP_DISTANCES_WEBGL,
|
||||
MAX_CULL_DISTANCES: ext.MAX_CULL_DISTANCES_WEBGL,
|
||||
MAX_COMBINED_CLIP_AND_CULL_DISTANCES: ext.MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL,
|
||||
|
||||
CLIP_DISTANCE0: ext.CLIP_DISTANCE0_WEBGL,
|
||||
CLIP_DISTANCE1: ext.CLIP_DISTANCE1_WEBGL,
|
||||
CLIP_DISTANCE2: ext.CLIP_DISTANCE2_WEBGL,
|
||||
CLIP_DISTANCE3: ext.CLIP_DISTANCE3_WEBGL,
|
||||
CLIP_DISTANCE4: ext.CLIP_DISTANCE4_WEBGL,
|
||||
CLIP_DISTANCE5: ext.CLIP_DISTANCE5_WEBGL,
|
||||
CLIP_DISTANCE6: ext.CLIP_DISTANCE6_WEBGL,
|
||||
CLIP_DISTANCE7: ext.CLIP_DISTANCE7_WEBGL
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_conservative_depth/
|
||||
*/
|
||||
export interface COMPAT_conservative_depth {
|
||||
}
|
||||
|
||||
export function getConservativeDepth(gl: GLRenderingContext): COMPAT_conservative_depth | null {
|
||||
if (isWebGL2(gl)) {
|
||||
const ext = gl.getExtension('EXT_conservative_depth');
|
||||
if (ext) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/WEBGL_stencil_texturing/
|
||||
*/
|
||||
export interface COMPAT_stencil_texturing {
|
||||
readonly DEPTH_STENCIL_TEXTURE_MODE: number;
|
||||
readonly STENCIL_INDEX: number;
|
||||
}
|
||||
|
||||
export function getStencilTexturing(gl: GLRenderingContext): COMPAT_stencil_texturing | null {
|
||||
if (isWebGL2(gl)) {
|
||||
const ext = gl.getExtension('WEBGL_stencil_texturing');
|
||||
if (ext) {
|
||||
return {
|
||||
DEPTH_STENCIL_TEXTURE_MODE: ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL,
|
||||
STENCIL_INDEX: ext.STENCIL_INDEX_WEBGL
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_clip_control/
|
||||
*/
|
||||
export interface COMPAT_clip_control {
|
||||
readonly LOWER_LEFT: number;
|
||||
readonly UPPER_LEFT: number;
|
||||
|
||||
readonly NEGATIVE_ONE_TO_ONE: number;
|
||||
readonly ZERO_TO_ONE: number;
|
||||
|
||||
readonly CLIP_ORIGIN: number;
|
||||
readonly CLIP_DEPTH_MODE: number;
|
||||
|
||||
/**
|
||||
* @param origin must be LOWER_LEFT (default) or UPPER_LEFT.
|
||||
* @param depth must be NEGATIVE_ONE_TO_ONE (default) or ZERO_TO_ONE.
|
||||
*/
|
||||
clipControl(origin: number, depth: number): void
|
||||
}
|
||||
|
||||
export function getClipControl(gl: GLRenderingContext): COMPAT_clip_control | null {
|
||||
const ext = gl.getExtension('EXT_clip_control');
|
||||
if (ext) {
|
||||
return {
|
||||
LOWER_LEFT: ext.LOWER_LEFT_EXT,
|
||||
UPPER_LEFT: ext.UPPER_LEFT_EXT,
|
||||
|
||||
NEGATIVE_ONE_TO_ONE: ext.NEGATIVE_ONE_TO_ONE_EXT,
|
||||
ZERO_TO_ONE: ext.ZERO_TO_ONE_EXT,
|
||||
|
||||
CLIP_ORIGIN: ext.CLIP_ORIGIN_EXT,
|
||||
CLIP_DEPTH_MODE: ext.CLIP_DEPTH_MODE_EXT,
|
||||
|
||||
clipControl: ext.clipControlEXT.bind(ext)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNoNonInstancedActiveAttribs(gl: GLRenderingContext): boolean {
|
||||
if (!isWebGL2(gl)) return false;
|
||||
|
||||
|
||||
@@ -52,6 +52,16 @@ export function checkError(gl: GLRenderingContext) {
|
||||
}
|
||||
}
|
||||
|
||||
export function glEnumToString(gl: GLRenderingContext, value: number) {
|
||||
const keys: string[] = [];
|
||||
for (const key in gl) {
|
||||
if ((gl as any)[key] === value) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
return keys.length ? keys.join(' | ') : `0x${value.toString(16)}`;
|
||||
}
|
||||
|
||||
function unbindResources(gl: GLRenderingContext) {
|
||||
// bind null to all texture units
|
||||
const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
|
||||
@@ -227,7 +237,7 @@ export interface WebGLContext {
|
||||
|
||||
export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScale: number }> = {}): WebGLContext {
|
||||
const extensions = createExtensions(gl);
|
||||
const state = createState(gl);
|
||||
const state = createState(gl, extensions);
|
||||
const stats = createStats();
|
||||
const resources = createResources(gl, state, stats, extensions);
|
||||
const timer = createTimer(gl, extensions, stats);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user