mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 22:31:26 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5af6a3e967 | ||
|
|
e718835042 | ||
|
|
70f0804e26 | ||
|
|
e613a90754 | ||
|
|
dca6affc84 | ||
|
|
af33516107 | ||
|
|
b11e24cd06 | ||
|
|
1a1bce8193 | ||
|
|
b67f7271fc | ||
|
|
837b838766 | ||
|
|
7dd42421e2 | ||
|
|
258dc637fc | ||
|
|
d6c594395c | ||
|
|
746173fe33 | ||
|
|
e9de12e6a2 | ||
|
|
3bfebceaea | ||
|
|
443fc9c60c | ||
|
|
2124bead5e | ||
|
|
a6077c7263 | ||
|
|
6f478a3eb3 | ||
|
|
a36c2feee4 | ||
|
|
a5e2946aa6 | ||
|
|
00428254a8 | ||
|
|
53a57530c5 | ||
|
|
5ee6bee130 | ||
|
|
734b6001c2 | ||
|
|
e88c2df42f | ||
|
|
79e6a4c95d | ||
|
|
2f56b9c491 |
@@ -1,3 +1,4 @@
|
||||
node_modules/*
|
||||
build/*
|
||||
docs/site/*
|
||||
lib/*
|
||||
@@ -5,7 +5,14 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v4.2.0] - 2023-04-05
|
||||
## [v4.3.0] - 2023-05-26
|
||||
|
||||
- Fix State Snapshots export animation (#1140)
|
||||
- Add depth of field (dof) postprocessing effect
|
||||
- Add `SbNcbrTunnels` extension for for visualizing tunnels in molecular structures from ChannelsDB (more info in [tunnels.md](./docs/docs/extensions/tunnels.md))
|
||||
- Fix edge case in minimizing RMSD transform computation
|
||||
|
||||
## [v4.2.0] - 2023-05-04
|
||||
|
||||
- Add emissive material support
|
||||
- Add bloom post-processing
|
||||
|
||||
118
docs/docs/extensions/tunnels.md
Normal file
118
docs/docs/extensions/tunnels.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Tunnel Visualization Extension
|
||||
This documentation outlines the usage of the Mol* extension for visualizing tunnels in molecular structures. The extension integrates with Mol* to render 3D representations of tunnels using specified data sources and properties.
|
||||
|
||||
The extension is a key component in ChannelsDB (https://channelsdb2.biodata.ceitec.cz/), enabling users to visualize tunnels within molecules directly from the database. While it is used with ChannelsDB, users can also input their own data or connect to different databases, ensuring versatility across various research environments.
|
||||
|
||||
## Data Types
|
||||
The primary data types involved in tunnel visualization are:
|
||||
|
||||
### Tunnel
|
||||
A Tunnel object contains the actual tunnel data necessary for visualization. It consists of:
|
||||
|
||||
- `data`: An array of `Profile` objects that describe the tunnel at various points.
|
||||
- `props`: Properties such as the tunnel's type, ID, and optional labels or descriptions.
|
||||
|
||||
### Profile
|
||||
A `Profile` object in a `Tunnel` holds detailed geometric and physical properties of a tunnel at specific points along its length. These properties include:
|
||||
|
||||
- `Charge`: The electric charge at a specific point in the tunnel.
|
||||
- `Radius`: The overall radius of the tunnel at this point.
|
||||
- `FreeRadius`: The radius of the tunnel not obstructed by any molecular elements.
|
||||
- `T`: Temperature factor or a similar property related to the point.
|
||||
- `Distance`: Distance along the tunnel's path from the start.
|
||||
- `X`, `Y`, `Z`: Coordinates of the point in 3D space.
|
||||
|
||||
These profiles are crucial for understanding the physical and chemical environment inside the tunnel, allowing for detailed analysis and visualization.
|
||||
|
||||
Example:
|
||||
```json
|
||||
"Profile": [
|
||||
{
|
||||
"Radius": 1.49,
|
||||
"FreeRadius": 1.49,
|
||||
"T": 0,
|
||||
"Distance": 0,
|
||||
"X": -19.152,
|
||||
"Y": -22.654,
|
||||
"Z": -13.034,
|
||||
"Charge": 0
|
||||
},
|
||||
{
|
||||
"Radius": 1.524,
|
||||
"FreeRadius": 1.524,
|
||||
"T": 0.00625,
|
||||
"Distance": 0.087,
|
||||
"X": -19.162,
|
||||
"Y": -22.596,
|
||||
"Z": -12.969,
|
||||
"Charge": 0
|
||||
},
|
||||
{
|
||||
"Radius": 1.56,
|
||||
"FreeRadius": 1.56,
|
||||
"T": 0.0125,
|
||||
"Distance": 0.174,
|
||||
"X": -19.171,
|
||||
"Y": -22.539,
|
||||
"Z": -12.905,
|
||||
"Charge": 0
|
||||
}
|
||||
]
|
||||
```
|
||||
## Transformers Usage
|
||||
The extension uses several transformations to process and visualize tunnel data:
|
||||
|
||||
### Tunnels Data Transformer
|
||||
- `Purpose`: Converts a collection of Tunnel data into a state object.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.toRoot().apply(TunnelsFromRawData, { data: tunnels });
|
||||
```
|
||||
|
||||
### Tunnel Data Provider
|
||||
- `Purpose`: Converts single Tunnel data into a state object for individual processing.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.toRoot().apply(TunnelFromRawData, {
|
||||
data: {
|
||||
data: tunnel.Profile,
|
||||
props: { id: tunnel.Id, type: tunnel.Type }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Tunnel Shape Provider
|
||||
- `Purpose`: Provides the shapes for rendering the tunnel based on WebGL context and shape parameters.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
}).apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
|
||||
## Visualization Examples
|
||||
To help users understand how to use these transformations in practice, include detailed examples:
|
||||
|
||||
### Visualizing Multiple Tunnels
|
||||
This example ([runVisualizeTunnels](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L19)) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, { webgl })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
|
||||
### Visualizing a Single Tunnel
|
||||
This example ([runVisualizeTunnel](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L46)) shows how to visualize a single tunnel.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelFromRawData, {
|
||||
data: {
|
||||
data: tunnel.Profile,
|
||||
props: { id: tunnel.Id, type: tunnel.Type }
|
||||
}
|
||||
})
|
||||
.apply(TunnelShapeProvider, { webgl })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
@@ -53,6 +53,7 @@ nav:
|
||||
- Extensions:
|
||||
- MolViewSpec: 'extensions/mvs/index.md'
|
||||
- wwPDB StructConn: 'extensions/struct-conn.md'
|
||||
- Tunnels: 'extensions/tunnels.md'
|
||||
- Misc:
|
||||
- Interesting PDB entries: misc/interesting-pdb-entries.md
|
||||
repo_url: https://github.com/molstar/docs
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "molstar",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.16",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -106,7 +106,8 @@
|
||||
"Yakov Pechersky <ffxen158@gmail.com>",
|
||||
"Christian Dominguez <christian.99dominguez@gmail.com>",
|
||||
"Cai Huiyu <szmun.caihy@gmail.com>",
|
||||
"Ryan DiRisio <rjdiris@gmail.com>"
|
||||
"Ryan DiRisio <rjdiris@gmail.com>",
|
||||
"Dušan Veľký <dvelky@mail.muni.cz>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
@@ -81,6 +81,7 @@ export const ExtensionMap = {
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
|
||||
@@ -72,10 +72,10 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
await params.pass.updateBackground();
|
||||
|
||||
await plugin.managers.animation.play(params.animation.definition, params.animation.params);
|
||||
|
||||
stoppedAnimation = false;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true });
|
||||
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true, updateControls: true });
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dominik Tichý <tichydominik451@gmail.com>
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
export { SbNcbrPartialCharges } from './partial-charges/behavior';
|
||||
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
export { SbNcbrTunnels } from './tunnels/behavior';
|
||||
export { TunnelsFromRawData, SelectTunnel, TunnelFromRawData, TunnelShapeProvider } from './tunnels/representation';
|
||||
99
src/extensions/sb-ncbr/tunnels/actions.ts
Normal file
99
src/extensions/sb-ncbr/tunnels/actions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateAction } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
|
||||
import { TunnelsServerConfig } from './props';
|
||||
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider } from './representation';
|
||||
|
||||
export const TunnelDownloadServer = {
|
||||
'channelsdb': PD.EmptyGroup({ label: 'ChannelsDB' })
|
||||
};
|
||||
|
||||
type DownloadTunnels = typeof DownloadTunnels
|
||||
export const DownloadTunnels = StateAction.build({
|
||||
display: { name: 'Download Tunnels', description: 'Load a tunnels from the provided source and create theirs representations' },
|
||||
from: PluginStateObject.Root,
|
||||
params: (_, plugin: PluginContext) => {
|
||||
return {
|
||||
source: PD.MappedStatic('pdb', {
|
||||
'pdb': PD.Group({
|
||||
provider: PD.Group({
|
||||
id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma/space separated PDB ids.' }),
|
||||
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
|
||||
}, { pivot: 'id' }),
|
||||
}, { isFlat: true, label: 'PDB' }),
|
||||
'alphafolddb': PD.Group({
|
||||
provider: PD.Group({
|
||||
id: PD.Text('Q8W3K0', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
|
||||
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
|
||||
}, { pivot: 'id' })
|
||||
}, { isFlat: true, label: 'AlphaFold DB', description: 'Loads the predicted model if available' }),
|
||||
'url': PD.Group({
|
||||
url: PD.Url(''),
|
||||
}, { isFlat: true, label: 'URL' })
|
||||
})
|
||||
};
|
||||
}
|
||||
})(({ params, state }, plugin: PluginContext) => Task.create('Download Tunnels', async ctx => {
|
||||
plugin.behaviors.layout.leftPanelTabName.next('data');
|
||||
|
||||
const src = params.source;
|
||||
let downloadParams: PD.Normalize<{ url: string | Asset.Url }>[];
|
||||
|
||||
switch (src.name) {
|
||||
case 'url':
|
||||
downloadParams = [{ url: src.params.url }];
|
||||
break;
|
||||
case 'pdb':
|
||||
downloadParams = src.params.provider.server.name === 'channelsdb'
|
||||
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/pdb/${src.params.provider.id}` }]
|
||||
: assertUnreachable(src as never);
|
||||
break;
|
||||
case 'alphafolddb':
|
||||
downloadParams = src.params.provider.server.name === 'channelsdb'
|
||||
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/alphafill/${src.params.provider.id.toLowerCase()}` }]
|
||||
: assertUnreachable(src as never);
|
||||
break;
|
||||
default: assertUnreachable(src);
|
||||
}
|
||||
|
||||
await state.transaction(async () => {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
for (const download of downloadParams) {
|
||||
const response = await (await fetch(download.url.toString())).json();
|
||||
const tunnels: Tunnel[] = [];
|
||||
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
}).runInContext(ctx);
|
||||
}));
|
||||
607
src/extensions/sb-ncbr/tunnels/algorithm.ts
Normal file
607
src/extensions/sb-ncbr/tunnels/algorithm.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { OrderedSet } from '../../../mol-data/int';
|
||||
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/algorithm';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { PositionData, Sphere3D, Box3D, GridLookup3D, fillGridDim } from '../../../mol-math/geometry';
|
||||
import { Boundary, getBoundary } from '../../../mol-math/geometry/boundary';
|
||||
import { DefaultMolecularSurfaceCalculationProps, MolecularSurfaceCalculationProps } from '../../../mol-math/geometry/molecular-surface';
|
||||
import { lerp, spline } from '../../../mol-math/interpolate';
|
||||
import { Vec3, Tensor, Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { Shape } from '../../../mol-model/shape';
|
||||
import { ensureReasonableResolution } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { Task, RuntimeContext } from '../../../mol-task';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Tunnel, Profile } from './data-model';
|
||||
|
||||
type MolecularSurfaceMeta = {
|
||||
resolution?: number
|
||||
colorTexture?: Texture
|
||||
}
|
||||
|
||||
export async function createSpheresShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, showRadii: boolean, prev?: Shape<Mesh> }) {
|
||||
const builder = MeshBuilder.createState(512, 512, options.prev?.geometry);
|
||||
const tunnel = options.tunnel;
|
||||
|
||||
const processedData = interpolateTunnel(tunnel.data, options.sampleRate);
|
||||
|
||||
if (options.showRadii) {
|
||||
for (let i = 0; i < processedData.length; i += 1) {
|
||||
const p = processedData[i];
|
||||
builder.currentGroup = i;
|
||||
const center = [p.X, p.Y, p.Z];
|
||||
addSphere(builder, center as Vec3, p.Radius, options.resolution);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < processedData.length; i += 1) {
|
||||
const p = processedData[i];
|
||||
builder.currentGroup = 0;
|
||||
const center = [p.X, p.Y, p.Z];
|
||||
addSphere(builder, center as Vec3, p.Radius, options.resolution);
|
||||
}
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(builder);
|
||||
const name = tunnel.props.highlight_label ?
|
||||
tunnel.props.highlight_label :
|
||||
tunnel.props.type && tunnel.props.id ?
|
||||
`${tunnel.props.type} ${tunnel.props.id}` :
|
||||
'Tunnel';
|
||||
|
||||
if (options.showRadii)
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
(i) => `[${processedData[i].X.toFixed(3)}, ${processedData[i].Y.toFixed(3)}, ${processedData[i].Z.toFixed(3)}] - radius: ${processedData[i].Radius.toFixed(3)}`,
|
||||
);
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
() => name,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createTunnelShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, webgl: WebGLContext | undefined, prev?: Shape<Mesh> }) {
|
||||
const tunnel = options.tunnel;
|
||||
const mesh = await createTunnelMesh(tunnel.data, {
|
||||
detail: options.resolution,
|
||||
sampleRate: options.sampleRate,
|
||||
webgl: options.webgl,
|
||||
prev: options.prev?.geometry
|
||||
});
|
||||
|
||||
const name = tunnel.props.highlight_label ?
|
||||
tunnel.props.highlight_label :
|
||||
tunnel.props.type && tunnel.props.id ?
|
||||
`${tunnel.props.type} ${tunnel.props.id}` :
|
||||
'Tunnel';
|
||||
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
() => name,
|
||||
);
|
||||
}
|
||||
|
||||
function profileToVec3(profile: Profile): Vec3 {
|
||||
return Vec3.create(profile.X, profile.Y, profile.Z);
|
||||
}
|
||||
|
||||
// Centripetal Catmull–Rom spline interpolation
|
||||
function interpolateTunnel(profile: Profile[], sampleRate: number) {
|
||||
const interpolatedProfiles: Profile[] = [];
|
||||
if (profile.length < 4) return profile; // Ensuring there are enough points to interpolate
|
||||
|
||||
interpolatedProfiles.push(profile[0]);
|
||||
|
||||
let lastPoint = profileToVec3(profile[0]);
|
||||
let currentDistance = 0;
|
||||
const pointInterval = 1 / sampleRate;
|
||||
|
||||
for (let i = 1; i < profile.length - 2; i++) {
|
||||
const P0 = profile[i - 1];
|
||||
const P1 = profile[i];
|
||||
const P2 = profile[i + 1];
|
||||
const P3 = profile[i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 0.05) {
|
||||
const interpolatedX = spline(P0.X, P1.X, P2.X, P3.X, t, 0.5);
|
||||
const interpolatedY = spline(P0.Y, P1.Y, P2.Y, P3.Y, t, 0.5);
|
||||
const interpolatedZ = spline(P0.Z, P1.Z, P2.Z, P3.Z, t, 0.5);
|
||||
const interpolatedPoint = Vec3.create(interpolatedX, interpolatedY, interpolatedZ);
|
||||
|
||||
const distanceToAdd = Vec3.distance(lastPoint, interpolatedPoint);
|
||||
currentDistance += distanceToAdd;
|
||||
|
||||
if (currentDistance >= pointInterval) {
|
||||
interpolatedProfiles.push({
|
||||
X: interpolatedX,
|
||||
Y: interpolatedY,
|
||||
Z: interpolatedZ,
|
||||
Radius: spline(P0.Radius, P1.Radius, P2.Radius, P3.Radius, t, 0.5),
|
||||
Charge: lerp(P1.Charge, P2.Charge, t),
|
||||
FreeRadius: spline(P0.FreeRadius, P1.FreeRadius, P2.FreeRadius, P3.FreeRadius, t, 0.5),
|
||||
T: lerp(P1.T, P2.T, t),
|
||||
Distance: lerp(P1.Distance, P2.Distance, t)
|
||||
});
|
||||
lastPoint = interpolatedPoint;
|
||||
currentDistance -= pointInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensuring the last profile point is included
|
||||
interpolatedProfiles.push(profile[profile.length - 1]);
|
||||
|
||||
return interpolatedProfiles;
|
||||
}
|
||||
|
||||
|
||||
function convertToPositionData(profile: Profile[], probeRadius: number): Required<PositionData> {
|
||||
let position = {} as PositionData;
|
||||
|
||||
const x: number[] = [];
|
||||
const y: number[] = [];
|
||||
const z: number[] = [];
|
||||
const indices: Array<number> = [];
|
||||
const radius: number[] = [];
|
||||
|
||||
let maxRadius: number = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
let sphereCounter = 0;
|
||||
for (const sphere of profile) {
|
||||
x.push(sphere.X);
|
||||
y.push(sphere.Y);
|
||||
z.push(sphere.Z);
|
||||
indices.push(sphereCounter);
|
||||
radius.push(sphere.Radius + probeRadius);
|
||||
if (sphere.Radius > maxRadius) maxRadius = sphere.Radius;
|
||||
sphereCounter++;
|
||||
}
|
||||
|
||||
position = { x, y, z, indices: OrderedSet.ofSortedArray(indices), radius, id: indices };
|
||||
|
||||
return position as Required<PositionData>;
|
||||
}
|
||||
|
||||
async function createTunnelMesh(
|
||||
data: Profile[],
|
||||
options: {
|
||||
detail: number,
|
||||
sampleRate: number,
|
||||
webgl?: WebGLContext,
|
||||
prev?: Mesh
|
||||
}
|
||||
) {
|
||||
const props = {
|
||||
...DefaultMolecularSurfaceCalculationProps,
|
||||
};
|
||||
const preprocessedData = interpolateTunnel(data, options.sampleRate);
|
||||
const positions = convertToPositionData(preprocessedData, props.probeRadius);
|
||||
const bounds: Boundary = getBoundary(positions);
|
||||
|
||||
let maxR = 0;
|
||||
for (let i = 0; i < positions.radius.length; ++i) {
|
||||
const r = positions.radius[i];
|
||||
if (maxR < r) maxR = r;
|
||||
}
|
||||
|
||||
const p = ensureReasonableResolution(bounds.box, props);
|
||||
|
||||
const { field, transform, /* resolution,*/ maxRadius, /* idField */ } = await computeTunnelSurface(
|
||||
{
|
||||
positions,
|
||||
boundary: bounds,
|
||||
maxRadius: maxR,
|
||||
box: bounds.box,
|
||||
props: p
|
||||
}
|
||||
).run();
|
||||
|
||||
const params = {
|
||||
isoLevel: p.probeRadius,
|
||||
scalarField: field,
|
||||
};
|
||||
const surface = await computeMarchingCubesMesh(params, options.prev).run();
|
||||
const iterations = Math.ceil(2 / 1);
|
||||
Mesh.smoothEdges(surface, { iterations, maxNewEdgeLength: Math.sqrt(2) });
|
||||
|
||||
Mesh.transform(surface, transform);
|
||||
if (options.webgl && !options.webgl.isWebGL2) {
|
||||
Mesh.uniformTriangleGroup(surface);
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, false);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, true);
|
||||
}
|
||||
|
||||
const sphere = Sphere3D.expand(Sphere3D(), bounds.sphere, maxRadius);
|
||||
surface.setBoundingSphere(sphere);
|
||||
(surface.meta as MolecularSurfaceMeta).resolution = options.detail;
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
function normalToLine(out: Vec3, p: Vec3) {
|
||||
out[0] = out[1] = out[2] = 1.0;
|
||||
if (p[0] !== 0) {
|
||||
out[0] = (p[1] + p[2]) / -p[0];
|
||||
} else if (p[1] !== 0) {
|
||||
out[1] = (p[0] + p[2]) / -p[1];
|
||||
} else if (p[2] !== 0) {
|
||||
out[2] = (p[0] + p[1]) / -p[2];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function computeTunnelSurface(
|
||||
surfaceData: {
|
||||
positions: Required<PositionData>,
|
||||
boundary: Boundary,
|
||||
maxRadius: number,
|
||||
box: Box3D | null,
|
||||
props: MolecularSurfaceCalculationProps
|
||||
}
|
||||
) {
|
||||
return Task.create('Tunnel Surface', async (ctx) => {
|
||||
return await calcTunnelSurface(ctx, surfaceData);
|
||||
});
|
||||
}
|
||||
|
||||
type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
|
||||
function getAngleTables(probePositions: number): AnglesTables {
|
||||
let theta = 0.0;
|
||||
const step = 2 * Math.PI / probePositions;
|
||||
|
||||
const cosTable = new Float32Array(probePositions);
|
||||
const sinTable = new Float32Array(probePositions);
|
||||
for (let i = 0; i < probePositions; i++) {
|
||||
cosTable[i] = Math.cos(theta);
|
||||
sinTable[i] = Math.sin(theta);
|
||||
theta += step;
|
||||
}
|
||||
return { cosTable, sinTable };
|
||||
}
|
||||
|
||||
// From '../../../\mol-math\geometry\molecular-surface.ts'
|
||||
async function calcTunnelSurface(ctx: RuntimeContext,
|
||||
surfaceData: {
|
||||
positions: Required<PositionData>,
|
||||
boundary: Boundary,
|
||||
maxRadius: number,
|
||||
box: Box3D | null,
|
||||
props: MolecularSurfaceCalculationProps
|
||||
}) {
|
||||
// Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
|
||||
// Other parts based heavily on NGL (Alexander Rose) EDT Surface class
|
||||
|
||||
let lastClip = -1;
|
||||
|
||||
/**
|
||||
* Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
|
||||
* Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
|
||||
*
|
||||
* Cache the last clipped atom (as very often the same one in subsequent calls)
|
||||
*
|
||||
* `a` and `b` must be resolved indices
|
||||
*/
|
||||
function obscured(x: number, y: number, z: number, a: number, b: number) {
|
||||
if (lastClip !== -1) {
|
||||
const ai = lastClip;
|
||||
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
|
||||
return ai;
|
||||
} else {
|
||||
lastClip = -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
|
||||
const ai = OrderedSet.getAt(indices, neighbours.indices[j]);
|
||||
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
|
||||
lastClip = ai;
|
||||
return ai;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ai` must be a resolved index
|
||||
*/
|
||||
function singleAtomObscures(ai: number, x: number, y: number, z: number) {
|
||||
const r = radius[ai];
|
||||
const dx = px[ai] - x;
|
||||
const dy = py[ai] - y;
|
||||
const dz = pz[ai] - z;
|
||||
const dSq = dx * dx + dy * dy + dz * dz;
|
||||
return dSq < (r * r);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each atom:
|
||||
* Iterate over a subsection of the grid, for each point:
|
||||
* If current value < 0.0, unvisited, set positive
|
||||
*
|
||||
* In any case: Project this point onto surface of the atomic sphere
|
||||
* If this projected point is not obscured by any other atom
|
||||
* Calculate delta distance and set grid value to minimum of
|
||||
* itself and delta
|
||||
*/
|
||||
function projectPointsRange(begI: number, endI: number) {
|
||||
for (let i = begI; i < endI; ++i) {
|
||||
const j = OrderedSet.getAt(indices, i);
|
||||
const vx = px[j], vy = py[j], vz = pz[j];
|
||||
const rad = radius[j];
|
||||
const rSq = rad * rad;
|
||||
|
||||
lookup3d.find(vx, vy, vz, rad);
|
||||
|
||||
// Number of grid points, round this up...
|
||||
const ng = Math.ceil(rad * scaleFactor);
|
||||
|
||||
// Center of the atom, mapped to grid points (take floor)
|
||||
const iax = Math.floor(scaleFactor * (vx - minX));
|
||||
const iay = Math.floor(scaleFactor * (vy - minY));
|
||||
const iaz = Math.floor(scaleFactor * (vz - minZ));
|
||||
|
||||
// Extents of grid to consider for this atom
|
||||
const begX = Math.max(0, iax - ng);
|
||||
const begY = Math.max(0, iay - ng);
|
||||
const begZ = Math.max(0, iaz - ng);
|
||||
|
||||
// Add two to these points:
|
||||
// - iax are floor'd values so this ensures coverage
|
||||
// - these are loop limits (exclusive)
|
||||
const endX = Math.min(dimX, iax + ng + 2);
|
||||
const endY = Math.min(dimY, iay + ng + 2);
|
||||
const endZ = Math.min(dimZ, iaz + ng + 2);
|
||||
|
||||
for (let xi = begX; xi < endX; ++xi) {
|
||||
const dx = gridx[xi] - vx;
|
||||
const xIdx = xi * iuv;
|
||||
for (let yi = begY; yi < endY; ++yi) {
|
||||
const dy = gridy[yi] - vy;
|
||||
const dxySq = dx * dx + dy * dy;
|
||||
const xyIdx = yi * iu + xIdx;
|
||||
for (let zi = begZ; zi < endZ; ++zi) {
|
||||
const dz = gridz[zi] - vz;
|
||||
const dSq = dxySq + dz * dz;
|
||||
|
||||
if (dSq < rSq) {
|
||||
const idx = zi + xyIdx;
|
||||
|
||||
// if unvisited, make positive
|
||||
if (data[idx] < 0.0) data[idx] *= -1;
|
||||
|
||||
// Project on to the surface of the sphere
|
||||
// sp is the projected point ( dx, dy, dz ) * ( ra / d )
|
||||
const d = Math.sqrt(dSq);
|
||||
const ap = rad / d;
|
||||
const spx = dx * ap + vx;
|
||||
const spy = dy * ap + vy;
|
||||
const spz = dz * ap + vz;
|
||||
|
||||
if (obscured(spx, spy, spz, j, -1) === -1) {
|
||||
const dd = rad - d;
|
||||
if (dd < data[idx]) {
|
||||
data[idx] = dd;
|
||||
idData[idx] = id[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function projectPoints() {
|
||||
for (let i = 0; i < n; i += updateChunk) {
|
||||
projectPointsRange(i, Math.min(i + updateChunk, n));
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'projecting points', current: i, max: n });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vectors for Torus Projection
|
||||
const atob = Vec3();
|
||||
const mid = Vec3();
|
||||
const n1 = Vec3();
|
||||
const n2 = Vec3();
|
||||
/**
|
||||
* `a` and `b` must be resolved indices
|
||||
*/
|
||||
function projectTorus(a: number, b: number) {
|
||||
const rA = radius[a];
|
||||
const rB = radius[b];
|
||||
const dx = atob[0] = px[b] - px[a];
|
||||
const dy = atob[1] = py[b] - py[a];
|
||||
const dz = atob[2] = pz[b] - pz[a];
|
||||
const dSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
// This check now redundant as already done in AVHash.withinRadii
|
||||
// if (dSq > ((rA + rB) * (rA + rB))) { return }
|
||||
|
||||
const d = Math.sqrt(dSq);
|
||||
|
||||
// Find angle between a->b vector and the circle
|
||||
// of their intersection by cosine rule
|
||||
const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d);
|
||||
|
||||
// distance along a->b at intersection
|
||||
const dmp = rA * cosA;
|
||||
|
||||
Vec3.normalize(atob, atob);
|
||||
|
||||
// Create normal to line
|
||||
normalToLine(n1, atob);
|
||||
Vec3.normalize(n1, n1);
|
||||
|
||||
// Cross together for second normal vector
|
||||
Vec3.cross(n2, atob, n1);
|
||||
Vec3.normalize(n2, n2);
|
||||
|
||||
// r is radius of circle of intersection
|
||||
const rInt = Math.sqrt(rA * rA - dmp * dmp);
|
||||
|
||||
Vec3.scale(n1, n1, rInt);
|
||||
Vec3.scale(n2, n2, rInt);
|
||||
Vec3.scale(atob, atob, dmp);
|
||||
|
||||
mid[0] = atob[0] + px[a];
|
||||
mid[1] = atob[1] + py[a];
|
||||
mid[2] = atob[2] + pz[a];
|
||||
|
||||
lastClip = -1;
|
||||
|
||||
for (let i = 0; i < probePositions; ++i) {
|
||||
const cost = cosTable[i];
|
||||
const sint = sinTable[i];
|
||||
|
||||
const px = mid[0] + cost * n1[0] + sint * n2[0];
|
||||
const py = mid[1] + cost * n1[1] + sint * n2[1];
|
||||
const pz = mid[2] + cost * n1[2] + sint * n2[2];
|
||||
|
||||
if (obscured(px, py, pz, a, b) === -1) {
|
||||
const iax = Math.floor(scaleFactor * (px - minX));
|
||||
const iay = Math.floor(scaleFactor * (py - minY));
|
||||
const iaz = Math.floor(scaleFactor * (pz - minZ));
|
||||
|
||||
const begX = Math.max(0, iax - ngTorus);
|
||||
const begY = Math.max(0, iay - ngTorus);
|
||||
const begZ = Math.max(0, iaz - ngTorus);
|
||||
|
||||
const endX = Math.min(dimX, iax + ngTorus + 2);
|
||||
const endY = Math.min(dimY, iay + ngTorus + 2);
|
||||
const endZ = Math.min(dimZ, iaz + ngTorus + 2);
|
||||
|
||||
for (let xi = begX; xi < endX; ++xi) {
|
||||
const dx = px - gridx[xi];
|
||||
const xIdx = xi * iuv;
|
||||
|
||||
for (let yi = begY; yi < endY; ++yi) {
|
||||
const dy = py - gridy[yi];
|
||||
const dxySq = dx * dx + dy * dy;
|
||||
const xyIdx = yi * iu + xIdx;
|
||||
|
||||
for (let zi = begZ; zi < endZ; ++zi) {
|
||||
const dz = pz - gridz[zi];
|
||||
const dSq = dxySq + dz * dz;
|
||||
|
||||
const idx = zi + xyIdx;
|
||||
const current = data[idx];
|
||||
|
||||
if (current > 0.0 && dSq < (current * current)) {
|
||||
data[idx] = Math.sqrt(dSq);
|
||||
// Is this grid point closer to a or b?
|
||||
// Take dot product of atob and gridpoint->p (dx, dy, dz)
|
||||
const dp = dx * atob[0] + dy * atob[1] + dz * atob[2];
|
||||
idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function projectToriiRange(begI: number, endI: number) {
|
||||
for (let i = begI; i < endI; ++i) {
|
||||
const k = OrderedSet.getAt(indices, i);
|
||||
lookup3d.find(px[k], py[k], pz[k], radius[k]);
|
||||
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
|
||||
const l = OrderedSet.getAt(indices, neighbours.indices[j]);
|
||||
if (k < l) projectTorus(k, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function projectTorii() {
|
||||
for (let i = 0; i < n; i += updateChunk) {
|
||||
projectToriiRange(i, Math.min(i + updateChunk, n));
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'projecting torii', current: i, max: n });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.time('MolecularSurface')
|
||||
// console.time('MolecularSurface createState')
|
||||
const { resolution, probeRadius, probePositions } = surfaceData.props;
|
||||
const scaleFactor = 1 / resolution;
|
||||
const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
|
||||
|
||||
const cellSize = Vec3.create(surfaceData.maxRadius, surfaceData.maxRadius, surfaceData.maxRadius);
|
||||
Vec3.scale(cellSize, cellSize, 2);
|
||||
const lookup3d = GridLookup3D(surfaceData.positions, surfaceData.boundary, cellSize);
|
||||
const neighbours = lookup3d.result;
|
||||
if (surfaceData.box === null) surfaceData.box = lookup3d.boundary.box;
|
||||
|
||||
const { indices, x: px, y: py, z: pz, id, radius } = surfaceData.positions;
|
||||
const n = OrderedSet.size(indices);
|
||||
|
||||
const pad = surfaceData.maxRadius + resolution;
|
||||
const expandedBox = Box3D.expand(Box3D(), surfaceData.box, Vec3.create(pad, pad, pad));
|
||||
const [minX, minY, minZ] = expandedBox.min;
|
||||
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
|
||||
const dim = Box3D.size(Vec3(), scaledBox);
|
||||
Vec3.ceil(dim, dim);
|
||||
|
||||
const [dimX, dimY, dimZ] = dim;
|
||||
const iu = dimZ, iv = dimY, iuv = iu * iv;
|
||||
|
||||
const { cosTable, sinTable } = getAngleTables(probePositions);
|
||||
|
||||
const space = Tensor.Space(dim, [0, 1, 2], Float32Array);
|
||||
const data = space.create();
|
||||
const idData = space.create();
|
||||
|
||||
data.fill(-1001.0);
|
||||
idData.fill(-1);
|
||||
|
||||
const gridx = fillGridDim(dimX, minX, resolution);
|
||||
const gridy = fillGridDim(dimY, minY, resolution);
|
||||
const gridz = fillGridDim(dimZ, minZ, resolution);
|
||||
|
||||
const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(surfaceData.maxRadius, 3), 3) * scaleFactor)));
|
||||
// console.timeEnd('MolecularSurface createState')
|
||||
|
||||
// console.time('MolecularSurface projectPoints')
|
||||
await projectPoints();
|
||||
// console.timeEnd('MolecularSurface projectPoints')
|
||||
|
||||
// console.time('MolecularSurface projectTorii')
|
||||
await projectTorii();
|
||||
// console.timeEnd('MolecularSurface projectTorii')
|
||||
// console.timeEnd('MolecularSurface')
|
||||
|
||||
const field = Tensor.create(space, data);
|
||||
const idField = Tensor.create(space, idData);
|
||||
|
||||
const transform = Mat4.identity();
|
||||
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
|
||||
Mat4.setTranslation(transform, expandedBox.min);
|
||||
// console.log({ field, idField, transform, updateChunk })
|
||||
return { field, idField, transform, resolution, maxRadius: surfaceData.maxRadius };
|
||||
}
|
||||
105
src/extensions/sb-ncbr/tunnels/behavior.ts
Normal file
105
src/extensions/sb-ncbr/tunnels/behavior.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior';
|
||||
import { DownloadTunnels } from './actions';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { Model, Structure } from '../../../mol-model/structure';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { getTunnelsConfig, TunnelsDataParams } from './props';
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { Tunnel, ChannelsDBdata, TunnelDB } from './data-model';
|
||||
import { TunnelShapeProvider, TunnelFromRawData } from './representation';
|
||||
import { ColorGenerator } from '../../meshes/mesh-utils';
|
||||
|
||||
export const SbNcbrTunnels = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'sb-ncbr-tunnels',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'SB NCBR Tunnels',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
register(): void {
|
||||
this.ctx.state.data.actions.add(DownloadTunnels);
|
||||
this.ctx.builders.structure.representation.registerPreset(TunnelsPreset);
|
||||
}
|
||||
unregister() {
|
||||
this.ctx.state.data.actions.remove(DownloadTunnels);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(TunnelsPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export function isApplicable(structure?: Structure): boolean {
|
||||
return (
|
||||
!!structure && structure.models.length === 1 &&
|
||||
Model.hasPdbId(structure.models[0])
|
||||
);
|
||||
}
|
||||
|
||||
export const TunnelsPreset = StructureRepresentationPresetProvider({
|
||||
id: 'sb-ncbr-preset-structure-tunnels',
|
||||
display: {
|
||||
name: 'Tunnels', group: 'Annotation',
|
||||
description: 'Shows Tunnels from ChannelsDB contained in the structure.'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return isApplicable(a.data);
|
||||
},
|
||||
params: (a, plugin) => {
|
||||
return {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
...getConfiguredDefaultParams(plugin)
|
||||
};
|
||||
},
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
const response = await (await fetch(`${params.serverUrl}/channels/${params.serverType}/${structure.model.entryId.toLowerCase()}`)).json();
|
||||
const tunnels: Tunnel[] = [];
|
||||
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await tunnels.forEach(async (tunnel) => {
|
||||
await update
|
||||
.toRoot()
|
||||
.apply(TunnelFromRawData, { data: tunnel })
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
colorTheme: ColorGenerator.next().value,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
await update.commit();
|
||||
});
|
||||
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
|
||||
|
||||
return { components: preset.components, representations: { ...preset.representations, } };
|
||||
}
|
||||
});
|
||||
|
||||
function getConfiguredDefaultParams(plugin: PluginContext) {
|
||||
const config = getTunnelsConfig(plugin);
|
||||
const params = PD.clone(TunnelsDataParams);
|
||||
PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
|
||||
return params;
|
||||
}
|
||||
119
src/extensions/sb-ncbr/tunnels/data-model.ts
Normal file
119
src/extensions/sb-ncbr/tunnels/data-model.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export interface Profile {
|
||||
Charge: number,
|
||||
Radius: number,
|
||||
FreeRadius: number,
|
||||
T: number,
|
||||
Distance: number,
|
||||
X: number,
|
||||
Y: number,
|
||||
Z: number
|
||||
}
|
||||
|
||||
export interface Layerweightedproperties {
|
||||
Hydrophobicity: number,
|
||||
Hydropathy: number,
|
||||
Polarity: number,
|
||||
Mutability: number
|
||||
}
|
||||
|
||||
export interface LayerGeometry {
|
||||
MinRadius: number,
|
||||
MinFreeRadius: number,
|
||||
StartDistance: number,
|
||||
EndDistance: number,
|
||||
LocalMinimum: boolean,
|
||||
Bottleneck: boolean,
|
||||
bottleneck: boolean
|
||||
}
|
||||
|
||||
export interface Properties {
|
||||
Charge: number,
|
||||
NumPositives: number,
|
||||
NumNegatives: number,
|
||||
Hydrophobicity: number,
|
||||
Hydropathy: number,
|
||||
Polarity: number,
|
||||
Mutability: number
|
||||
}
|
||||
|
||||
export interface LayersInfo {
|
||||
LayerGeometry: LayerGeometry,
|
||||
Residues: string[],
|
||||
FlowIndices: string[],
|
||||
Properties: Properties
|
||||
}
|
||||
|
||||
export interface Layers {
|
||||
ResidueFlow: string[],
|
||||
HetResidues: any[],
|
||||
LayerWeightedProperties: Layerweightedproperties
|
||||
LayersInfo: LayersInfo[]
|
||||
}
|
||||
|
||||
export interface TunnelDB {
|
||||
Type: string,
|
||||
Id: string,
|
||||
Cavity: string,
|
||||
Auto: boolean,
|
||||
Properties: Properties,
|
||||
Profile: Profile[],
|
||||
Layers: Layers
|
||||
};
|
||||
|
||||
export interface ChannelsDBdata {
|
||||
'CSATunnels_MOLE': TunnelDB[],
|
||||
'CSATunnels_Caver': TunnelDB[],
|
||||
'ReviewedChannels_MOLE': TunnelDB[],
|
||||
'ReviewedChannels_Caver': TunnelDB[],
|
||||
'CofactorTunnels_MOLE': TunnelDB[],
|
||||
'CofactorTunnels_Caver': TunnelDB[],
|
||||
'TransmembranePores_MOLE': TunnelDB[],
|
||||
'TransmembranePores_Caver': TunnelDB[],
|
||||
'ProcognateTunnels_MOLE': TunnelDB[],
|
||||
'ProcognateTunnels_Caver': TunnelDB[],
|
||||
'AlphaFillTunnels_MOLE': TunnelDB[],
|
||||
'AlphaFillTunnels_Caver': TunnelDB[]
|
||||
}
|
||||
|
||||
export interface ChannelsCache {
|
||||
Channels: ChannelsDBdata
|
||||
}
|
||||
|
||||
export interface Tunnel {
|
||||
data: Profile[],
|
||||
props: {
|
||||
highlight_label?: string,
|
||||
type?: string,
|
||||
id?: string,
|
||||
label?: string,
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const TunnelShapeParams = {
|
||||
webgl: PD.Value<WebGLContext | null>(null),
|
||||
colorTheme: PD.Color(Color(0xff0000)),
|
||||
visual: PD.MappedStatic(
|
||||
'mesh',
|
||||
{
|
||||
mesh: PD.Group({ resolution: PD.Numeric(2) }),
|
||||
spheres: PD.Group({ resolution: PD.Numeric(2) })
|
||||
}
|
||||
),
|
||||
samplingRate: PD.Numeric(1, { min: 0.05, max: 1, step: 0.05 }),
|
||||
showRadii: PD.Boolean(false),
|
||||
};
|
||||
|
||||
export class TunnelStateObject extends PluginStateObject.Create<{ tunnel: Tunnel }>({ name: 'Tunnel Entry', typeClass: 'Data' }) { }
|
||||
export class TunnelsStateObject extends PluginStateObject.Create<{ tunnels: Tunnel[] }>({ name: 'Tunnels', typeClass: 'Data' }) { }
|
||||
63
src/extensions/sb-ncbr/tunnels/examples.ts
Normal file
63
src/extensions/sb-ncbr/tunnels/examples.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
|
||||
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider, TunnelFromRawData } from './representation';
|
||||
|
||||
|
||||
export const DB_URL = 'https://channelsdb2.biodata.ceitec.cz/api/channels/';
|
||||
export const SUB_DB = 'pdb';
|
||||
export const CHANNEL = '1ymg';
|
||||
|
||||
export const URL = `${DB_URL}${SUB_DB}/${CHANNEL}`;
|
||||
|
||||
export async function runVisualizeTunnels(plugin: PluginContext, url: string = URL) {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
const response = await (await fetch(url)).json();
|
||||
|
||||
const tunnels: Tunnel[] = [];
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
export async function runVisualizeTunnel(plugin: PluginContext) {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
const response = await (await fetch(URL)).json();
|
||||
|
||||
const tunnel = response.Channels.TransmembranePores_MOLE[0];
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelFromRawData, { data: { data: tunnel.Profile, props: { id: tunnel.Id, type: tunnel.Type } } })
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
27
src/extensions/sb-ncbr/tunnels/props.ts
Normal file
27
src/extensions/sb-ncbr/tunnels/props.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { PluginConfigItem } from '../../../mol-plugin/config';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export namespace TunnelsData {
|
||||
export const DefaultServerUrl = 'https://channelsdb2.biodata.ceitec.cz/api';
|
||||
export const DefaultServerType = 'pdb';
|
||||
}
|
||||
|
||||
export const TunnelsDataParams = {
|
||||
serverType: PD.Select('pdb', [['pdb', 'pdb']] as const),
|
||||
serverUrl: PD.Text(TunnelsData.DefaultServerUrl)
|
||||
};
|
||||
export type TunnelsDataParams = typeof TunnelsDataParams
|
||||
|
||||
export const TunnelsServerConfig = {
|
||||
DefaultServerUrl: new PluginConfigItem('channelsdb-server', 'https://channelsdb2.biodata.ceitec.cz/api'),
|
||||
DefaultServerType: new PluginConfigItem<'pdb'>('serverType', 'pdb')
|
||||
};
|
||||
|
||||
export function getTunnelsConfig(plugin: PluginContext): { [key in keyof typeof TunnelsServerConfig]: NonNullable<typeof TunnelsServerConfig[key]['defaultValue']> } {
|
||||
return {
|
||||
DefaultServerUrl: plugin.config.get(TunnelsServerConfig.DefaultServerUrl) ?? TunnelsServerConfig.DefaultServerUrl.defaultValue ?? TunnelsData.DefaultServerUrl,
|
||||
DefaultServerType: plugin.config.get(TunnelsServerConfig.DefaultServerType) ?? TunnelsServerConfig.DefaultServerType.defaultValue ?? TunnelsData.DefaultServerType,
|
||||
};
|
||||
}
|
||||
|
||||
102
src/extensions/sb-ncbr/tunnels/representation.ts
Normal file
102
src/extensions/sb-ncbr/tunnels/representation.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { StateTransformer } from '../../../mol-state';
|
||||
import { TunnelStateObject, Tunnel, TunnelShapeParams, TunnelsStateObject } from './data-model';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { createTunnelShape, createSpheresShape } from './algorithm';
|
||||
|
||||
const Transform = StateTransformer.builderFactory('sb-ncbr-tunnels');
|
||||
|
||||
export const TunnelsFromRawData = Transform({
|
||||
name: 'sb-ncbr-tunnels-from-data',
|
||||
display: { name: 'Tunnels' },
|
||||
from: PluginStateObject.Root,
|
||||
to: TunnelsStateObject,
|
||||
params: {
|
||||
data: PD.Value<Tunnel[]>([]),
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new TunnelsStateObject({ tunnels: params.data });
|
||||
},
|
||||
});
|
||||
|
||||
export const SelectTunnel = Transform({
|
||||
name: 'sb-ncbr-tunnel-from-tunnels',
|
||||
display: { name: 'Tunnel Selection' },
|
||||
from: TunnelsStateObject,
|
||||
to: TunnelStateObject,
|
||||
params: a => {
|
||||
return {
|
||||
index: PD.Numeric(0, { min: 0, max: a!.data.tunnels.length - 1, step: 1 })
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return new TunnelStateObject({ tunnel: a.data.tunnels[params.index] });
|
||||
}
|
||||
});
|
||||
|
||||
export const TunnelFromRawData = Transform({
|
||||
name: 'sb-ncbr-tunnel-from-data',
|
||||
display: { name: 'Tunnel Entry' },
|
||||
from: PluginStateObject.Root,
|
||||
to: TunnelStateObject,
|
||||
params: {
|
||||
data: PD.Value<Tunnel>(undefined as any, { isHidden: true })
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new TunnelStateObject({ tunnel: params.data });
|
||||
},
|
||||
});
|
||||
|
||||
export const TunnelShapeProvider = Transform({
|
||||
name: 'sb-ncbr-tunnel-shape-provider',
|
||||
display: { name: 'Tunnel' },
|
||||
from: TunnelStateObject,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: a => { return TunnelShapeParams; },
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Tunnel Shape Representation', async ctx => {
|
||||
return new PluginStateObject.Shape.Provider({
|
||||
label: 'Surface',
|
||||
data: { params, data: a.data },
|
||||
params: Mesh.Params,
|
||||
geometryUtils: Mesh.Utils,
|
||||
getShape: (_, data, __, mesh) => {
|
||||
if (data.params.visual.name === 'mesh' && !data.params.showRadii) {
|
||||
return createTunnelShape({
|
||||
tunnel: data.data.tunnel,
|
||||
color: data.params.colorTheme,
|
||||
resolution: data.params.visual.params.resolution,
|
||||
sampleRate: data.params.samplingRate,
|
||||
webgl: data.params.webgl, prev: mesh
|
||||
});
|
||||
}
|
||||
return createSpheresShape({
|
||||
tunnel: data.data.tunnel,
|
||||
color: data.params.colorTheme,
|
||||
resolution: data.params.visual.params.resolution,
|
||||
sampleRate: data.params.samplingRate,
|
||||
showRadii: data.params.showRadii, prev: mesh
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: a.data.tunnel.props.label ?? 'Tunnel',
|
||||
description: a.data.tunnel.props.description
|
||||
?? (a.data.tunnel.props.type && a.data.tunnel.props.id)
|
||||
? `${a.data.tunnel.props.type} ${a.data.tunnel.props.id}`
|
||||
: '',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -278,7 +278,7 @@ interface Canvas3D {
|
||||
* Function for external "animation" control
|
||||
* Calls commit.
|
||||
*/
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }): void
|
||||
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
@@ -527,9 +527,15 @@ namespace Canvas3D {
|
||||
|
||||
let animationFrameHandle = 0;
|
||||
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }) {
|
||||
currentTime = t;
|
||||
commit(options?.isSynchronous);
|
||||
|
||||
// update the controler before the camera transition
|
||||
if (options?.updateControls) {
|
||||
controls.update(currentTime);
|
||||
}
|
||||
|
||||
camera.transition.tick(currentTime);
|
||||
hiZ.tick();
|
||||
|
||||
|
||||
211
src/mol-canvas3d/passes/dof.ts
Normal file
211
src/mol-canvas3d/passes/dof.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { TextureSpec, UniformSpec, DefineSpec, 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, createNullTexture } from '../../mol-gl/webgl/texture';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } 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 { dof_frag } from '../../mol-gl/shader/dof.frag';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const DofParams = {
|
||||
blurSize: PD.Numeric(9, { min: 1, max: 32, step: 1 }),
|
||||
blurSpread: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
inFocus: PD.Numeric(0.0, { min: -5000.0, max: 5000.0, step: 1.0 }, { description: 'Distance from the scene center that will be in focus' }),
|
||||
PPM: PD.Numeric(20.0, { min: 0.0, max: 5000.0, step: 0.1 }, { description: 'Size of the area that will be in focus' }),
|
||||
center: PD.Select('camera-target', PD.arrayToOptions(['scene-center', 'camera-target'])),
|
||||
mode: PD.Select('plane', PD.arrayToOptions(['plane', 'sphere'])),
|
||||
};
|
||||
|
||||
export type DofProps = PD.Values<typeof DofParams>
|
||||
|
||||
export class DofPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.dof.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
private readonly renderable: DofRenderable;
|
||||
|
||||
constructor(private webgl: WebGLContext, width: number, height: number) {
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
|
||||
const nullTexture = createNullTexture();
|
||||
this.renderable = getDofRenderable(webgl, nullTexture, nullTexture, nullTexture);
|
||||
}
|
||||
|
||||
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) {
|
||||
const w = this.target.texture.getWidth();
|
||||
const h = this.target.texture.getHeight();
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, input: Texture, depthOpaque: Texture, depthTransparent: Texture, props: DofProps, sphere: Sphere3D) {
|
||||
let needsUpdate = false;
|
||||
if (this.renderable.values.tColor.ref.value !== input) {
|
||||
ValueCell.update(this.renderable.values.tColor, input);
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (this.renderable.values.tDepthOpaque.ref.value !== depthOpaque) {
|
||||
ValueCell.update(this.renderable.values.tDepthOpaque, depthOpaque);
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (this.renderable.values.tDepthTransparent.ref.value !== depthTransparent) {
|
||||
ValueCell.update(this.renderable.values.tDepthTransparent, depthTransparent);
|
||||
needsUpdate = true;
|
||||
}
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
const invProjection = this.renderable.values.uInvProjection.ref.value;
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
ValueCell.update(this.renderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.renderable.values.uMode, props.mode === 'sphere' ? 1 : 0);
|
||||
|
||||
Vec4.set(this.renderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.renderable.values.uBounds, this.renderable.values.uBounds.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
|
||||
|
||||
if (this.renderable.values.dBlurSize.ref.value !== props.blurSize) {
|
||||
ValueCell.update(this.renderable.values.dBlurSize, props.blurSize);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
const wolrdCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
|
||||
const distance = Vec3.distance(camera.state.position, wolrdCenter);
|
||||
const inFocus = distance + props.inFocus;
|
||||
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus);
|
||||
|
||||
// transform center in view space
|
||||
const center = this.renderable.values.uCenter.ref.value;
|
||||
Vec3.transformMat4(center, wolrdCenter, camera.view);
|
||||
ValueCell.update(this.renderable.values.uCenter, center);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBlurSpread, props.blurSpread);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM);
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(viewport: Viewport, target: undefined | RenderTarget) {
|
||||
if (isTimingMode) this.webgl.timer.mark('DofPass.render');
|
||||
if (target) {
|
||||
target.bind();
|
||||
} else {
|
||||
this.webgl.unbindFramebuffer();
|
||||
}
|
||||
this.updateState(viewport);
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('DofPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const DofSchema = {
|
||||
...QuadSchema,
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
uCenter: UniformSpec('v3'),
|
||||
uMode: UniformSpec('i'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
|
||||
dBlurSize: DefineSpec('number'),
|
||||
uBlurSpread: UniformSpec('f'),
|
||||
uInFocus: UniformSpec('f'),
|
||||
uPPM: UniformSpec('f'),
|
||||
};
|
||||
|
||||
const DofShaderCode = ShaderCode('dof', quad_vert, dof_frag);
|
||||
type DofRenderable = ComputeRenderable<Values<typeof DofSchema>>
|
||||
|
||||
function getDofRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture): DofRenderable {
|
||||
const width = colorTexture.getWidth();
|
||||
const height = colorTexture.getHeight();
|
||||
|
||||
const values: Values<typeof DofSchema> = {
|
||||
...QuadValues,
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
uCenter: ValueCell.create(Vec3()),
|
||||
uMode: ValueCell.create(0),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
|
||||
dBlurSize: ValueCell.create(5),
|
||||
uBlurSpread: ValueCell.create(300.0),
|
||||
uInFocus: ValueCell.create(20.0),
|
||||
uPPM: ValueCell.create(20.0),
|
||||
};
|
||||
|
||||
const schema = { ...DofSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', DofShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { MarkingPass, MarkingProps } from './marking';
|
||||
import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
|
||||
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { DofPass } from './dof';
|
||||
import { BloomPass } from './bloom';
|
||||
|
||||
type Props = {
|
||||
@@ -63,6 +64,7 @@ export class DrawPass {
|
||||
readonly postprocessing: PostprocessingPass;
|
||||
readonly antialiasing: AntialiasingPass;
|
||||
readonly bloom: BloomPass;
|
||||
readonly dof: DofPass;
|
||||
|
||||
private transparencyMode: TransparencyMode = 'blended';
|
||||
setTransparency(transparency: 'wboit' | 'dpoit' | 'blended') {
|
||||
@@ -107,6 +109,7 @@ export class DrawPass {
|
||||
this.postprocessing = new PostprocessingPass(webgl, assetManager, this);
|
||||
this.antialiasing = new AntialiasingPass(webgl, width, height);
|
||||
this.bloom = new BloomPass(webgl, width, height);
|
||||
this.dof = new DofPass(webgl, width, height);
|
||||
|
||||
this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
|
||||
this.copyFboPostprocessing = createCopyRenderable(webgl, this.postprocessing.target.texture);
|
||||
@@ -148,6 +151,7 @@ export class DrawPass {
|
||||
this.marking.setSize(width, height);
|
||||
this.postprocessing.setSize(width, height);
|
||||
this.antialiasing.setSize(width, height);
|
||||
this.dof.setSize(width, height);
|
||||
this.bloom.setSize(width, height);
|
||||
}
|
||||
|
||||
@@ -163,7 +167,7 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -217,7 +221,7 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -281,7 +285,7 @@ export class DrawPass {
|
||||
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -330,6 +334,7 @@ export class DrawPass {
|
||||
const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
|
||||
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
renderer.setViewport(x, y, width, height);
|
||||
@@ -388,12 +393,30 @@ export class DrawPass {
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
|
||||
let needsTargetCopy = false;
|
||||
|
||||
if (antialiasingEnabled) {
|
||||
const input = PostprocessingPass.isEnabled(props.postprocessing)
|
||||
? this.postprocessing.target.texture
|
||||
: this.colorTarget.texture;
|
||||
this.antialiasing.render(camera, input, toDrawingBuffer, props.postprocessing);
|
||||
} else if (toDrawingBuffer) {
|
||||
this.antialiasing.render(camera, input, toDrawingBuffer && !dofEnabled, props.postprocessing);
|
||||
} else if (toDrawingBuffer && !DofPass.isEnabled(props.postprocessing)) {
|
||||
needsTargetCopy = true;
|
||||
}
|
||||
|
||||
if (props.postprocessing.dof.name === 'on') {
|
||||
const input = AntialiasingPass.isEnabled(props.postprocessing)
|
||||
? this.antialiasing.target.texture
|
||||
: PostprocessingPass.isEnabled(props.postprocessing)
|
||||
? this.postprocessing.target.texture
|
||||
: this.colorTarget.texture;
|
||||
this.dof.update(camera, input, this.depthTargetOpaque?.texture || this.depthTextureOpaque, this.depthTextureTransparent, props.postprocessing.dof.params, scene.boundingSphereVisible);
|
||||
this.dof.render(camera.viewport, toDrawingBuffer ? undefined : this.getColorTarget(props.postprocessing));
|
||||
} else if (toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing)) {
|
||||
needsTargetCopy = true;
|
||||
}
|
||||
|
||||
if (needsTargetCopy) {
|
||||
this.drawTarget.bind();
|
||||
|
||||
this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
|
||||
@@ -448,7 +471,9 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {
|
||||
if (AntialiasingPass.isEnabled(postprocessingProps)) {
|
||||
if (DofPass.isEnabled(postprocessingProps)) {
|
||||
return this.dof.target;
|
||||
} else if (AntialiasingPass.isEnabled(postprocessingProps)) {
|
||||
return this.antialiasing.target;
|
||||
} else if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
return this.postprocessing.target;
|
||||
|
||||
@@ -34,6 +34,7 @@ import { AssetManager } from '../../mol-util/assets';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
|
||||
import { CasParams, CasPass } from './cas';
|
||||
import { DofParams } from './dof';
|
||||
import { BloomParams } from './bloom';
|
||||
|
||||
export const OutlinesSchema = {
|
||||
@@ -396,6 +397,10 @@ export const PostprocessingParams = {
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' }),
|
||||
dof: PD.MappedStatic('off', {
|
||||
on: PD.Group(DofParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'DOF' }),
|
||||
antialiasing: PD.MappedStatic('smaa', {
|
||||
fxaa: PD.Group(FxaaParams),
|
||||
smaa: PD.Group(SmaaParams),
|
||||
|
||||
120
src/mol-gl/shader/dof.frag.ts
Normal file
120
src/mol-gl/shader/dof.frag.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const dof_frag = `
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision highp sampler2D;
|
||||
|
||||
#include common
|
||||
|
||||
uniform sampler2D tColor;
|
||||
uniform sampler2D tDepthOpaque;
|
||||
uniform sampler2D tDepthTransparent;
|
||||
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
uniform float uBlurSpread;
|
||||
uniform float uInFocus;
|
||||
uniform float uPPM;
|
||||
|
||||
uniform float uNear; // Near plane
|
||||
uniform float uFar; // Far plane
|
||||
|
||||
uniform mat4 uInvProjection; // Inverse projection
|
||||
uniform mat4 uProjection; // projection
|
||||
|
||||
uniform int uMode; // 0-planar, 1-spherical
|
||||
uniform vec3 uCenter; // Center of focus sphere in view space
|
||||
|
||||
// Function to convert depth value from depth buffer to view space Z
|
||||
float getViewZ(const in float depth) {
|
||||
#if dOrthographic == 1
|
||||
return orthographicDepthToViewZ(depth, uNear, uFar);
|
||||
#else
|
||||
return perspectiveDepthToViewZ(depth, uNear, uFar);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Retrieve depth from opaque depth texture
|
||||
float getDepthOpaque(const in vec2 coords) {
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepthOpaque, coords).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepthOpaque, coords));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Retrieve depth from transparent depth texture
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getDepth(const in vec2 coords) {
|
||||
return min(getDepthOpaque(coords), getDepthTransparent(coords));
|
||||
}
|
||||
|
||||
float getCOC(vec2 uv) {
|
||||
float depth = getDepth(uv);
|
||||
float viewDist = getViewZ(depth);
|
||||
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
|
||||
float focusDist = length(aposition - uCenter);
|
||||
float coc = 0.0; // Circle of Confusion
|
||||
if (uMode == 0) { // planar Depth of field
|
||||
coc = (abs(viewDist) - uInFocus) / uPPM; //focus distance, focus range
|
||||
} else if(uMode == 1) { // spherical Depth of field
|
||||
coc = focusDist / uPPM ;
|
||||
}
|
||||
coc = clamp(coc, -1.0, 1.0);
|
||||
return coc;
|
||||
}
|
||||
|
||||
// Simple box blur for blurring the image
|
||||
vec3 getBlurredImage(vec2 coords) {
|
||||
vec4 blurColor = vec4(0);
|
||||
vec2 texelSize = vec2(1.0 / uTexSize.x, 1.0 / uTexSize.y);
|
||||
float count = 0.0;
|
||||
for (int x = 0; x < int(dBlurSize); x++) {
|
||||
for (int y = 0; y < int(dBlurSize); y++) {
|
||||
vec2 offset = vec2(float(x) - float(dBlurSize) / 2.0, float(y) - float(dBlurSize) / 2.0);
|
||||
vec2 uvPixel = coords.xy + offset * texelSize * uBlurSpread;
|
||||
float coc = getCOC(uvPixel);
|
||||
coc = smoothstep(0.0, 1.0, abs(coc));
|
||||
// mix blurColor with new color with weight coc
|
||||
blurColor.rgb = blurColor.rgb + texture2D(tColor, uvPixel).xyz * coc;
|
||||
count+=coc;
|
||||
}
|
||||
}
|
||||
blurColor = blurColor / count;
|
||||
return blurColor.rgb;
|
||||
}
|
||||
|
||||
// simplification from https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / uTexSize;
|
||||
vec4 color = texture2D(tColor, uv);
|
||||
float depth = getDepth(uv);
|
||||
|
||||
float viewDist = getViewZ(depth);
|
||||
|
||||
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
|
||||
float focusDist = length(aposition - uCenter);
|
||||
vec3 blurColor = getBlurredImage(uv);
|
||||
|
||||
float coc = getCOC(uv); // Circle of Confusion
|
||||
|
||||
// for debugging the coc
|
||||
// color.rgb = (coc < 0.0) ? (1.0 - abs(coc)) * vec3(1.0,0.0,0.0) : vec3(0.0, 1.0 - coc, 0.0) ;//mix(color.rgb, blurColor.rgb, abs(coc));
|
||||
color.rgb = mix(color.rgb, blurColor, smoothstep(0.0, 1.0, abs(coc))); // Smooth blending based on CoC
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`;
|
||||
@@ -344,6 +344,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
|
||||
contextRestored,
|
||||
setContextLost: () => {
|
||||
isContextLost = true;
|
||||
timer.clear();
|
||||
},
|
||||
handleContextRestored: (extraResets?: () => void) => {
|
||||
Object.assign(extensions, createExtensions(gl));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -33,6 +33,10 @@ class MovingAverage {
|
||||
return Object.fromEntries(this.avgs.entries());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.avgs.clear();
|
||||
}
|
||||
|
||||
constructor(private count: number) { }
|
||||
}
|
||||
|
||||
@@ -111,8 +115,13 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions,
|
||||
dtq.deleteQuery(query);
|
||||
});
|
||||
pending.clear();
|
||||
stack.length = 0;
|
||||
gpuAvgs.clear();
|
||||
cpuAvgs.clear();
|
||||
|
||||
measures = [];
|
||||
current = null;
|
||||
capturingStats = false;
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
|
||||
@@ -80,7 +80,8 @@ function computeN(state: RmsdTransformState) {
|
||||
|
||||
let sizeSq = 0.0;
|
||||
|
||||
for (let i = 0, _l = state.a.x.length; i < _l; i++) {
|
||||
const L = Math.min(state.a.x.length, state.b.x.length);
|
||||
for (let i = 0; i < L; i++) {
|
||||
const aX = xsA[i] - cA[0], aY = ysA[i] - cA[1], aZ = zsA[i] - cA[2];
|
||||
const bX = xsB[i] - cB[0], bY = ysB[i] - cB[1], bZ = zsB[i] - cB[2];
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ import { PluginStateAnimation } from '../model';
|
||||
async function setPartialSnapshot(plugin: PluginContext, entry: PluginStateSnapshotManager.Entry, first = false) {
|
||||
if (entry.snapshot.data) {
|
||||
await plugin.runTask(plugin.state.data.setSnapshot(entry.snapshot.data));
|
||||
// update the canvas3d trackball with the snapshot
|
||||
plugin.canvas3d?.setProps({
|
||||
trackball: entry.snapshot.canvas3d?.props?.trackball
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (entry.snapshot.camera) {
|
||||
plugin.canvas3d?.requestCameraReset({
|
||||
snapshot: entry.snapshot.camera.current,
|
||||
@@ -77,7 +83,7 @@ export const AnimateStateSnapshots = PluginStateAnimation.create({
|
||||
return { kind: 'skip' };
|
||||
}
|
||||
|
||||
setPartialSnapshot(ctx.plugin, animState.snapshots[i]);
|
||||
await setPartialSnapshot(ctx.plugin, animState.snapshots[i]);
|
||||
|
||||
return { kind: 'next', state: { ...animState, currentIndex: i } };
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ const SimpleSettingsParams = {
|
||||
occlusion: Canvas3DParams.postprocessing.params.occlusion,
|
||||
shadow: Canvas3DParams.postprocessing.params.shadow,
|
||||
outline: Canvas3DParams.postprocessing.params.outline,
|
||||
dof: Canvas3DParams.postprocessing.params.dof,
|
||||
fog: Canvas3DParams.cameraFog,
|
||||
}, { isFlat: true }),
|
||||
clipping: PD.Group<any>({
|
||||
@@ -129,6 +130,7 @@ const SimpleSettingsMapping = ParamMapping({
|
||||
occlusion: canvas.postprocessing.occlusion,
|
||||
shadow: canvas.postprocessing.shadow,
|
||||
outline: canvas.postprocessing.outline,
|
||||
dof: canvas.postprocessing.dof,
|
||||
fog: canvas.cameraFog,
|
||||
},
|
||||
clipping: {
|
||||
@@ -162,6 +164,7 @@ const SimpleSettingsMapping = ParamMapping({
|
||||
canvas.multiSample = s.advanced.multiSample;
|
||||
canvas.hiZ = s.advanced.hiZ;
|
||||
canvas.postprocessing.sharpening = s.advanced.sharpening;
|
||||
canvas.postprocessing.dof = s.lighting.dof;
|
||||
|
||||
props.layout = s.layout;
|
||||
props.pixelScale = s.advanced.pixelScale;
|
||||
|
||||
@@ -19,7 +19,7 @@ export class PluginAnimationLoop {
|
||||
return this._isAnimating;
|
||||
}
|
||||
|
||||
async tick(t: number, options?: { isSynchronous?: boolean, manualDraw?: boolean, animation?: PluginAnimationManager.AnimationInfo }) {
|
||||
async tick(t: number, options?: { isSynchronous?: boolean, manualDraw?: boolean, animation?: PluginAnimationManager.AnimationInfo, updateControls?: boolean}) {
|
||||
await this.plugin.managers.animation.tick(t, options?.isSynchronous, options?.animation);
|
||||
this.plugin.canvas3d?.tick(t as now.Timestamp, options);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user