Compare commits

...

41 Commits

Author SHA1 Message Date
dsehnal
add75bf9c9 5.0.0-dev.1 2025-07-28 16:37:17 +02:00
dsehnal
57cbcd5fbf npm audit 2025-07-28 16:34:08 +02:00
zachcp
0a33936e06 Update field-schema.ts to point directly to PathReporter (#1587) 2025-07-28 07:48:00 +02:00
Alexander Rose
7291025e09 Merge pull request #1585 from molstar/scene-scale
Scene scale
2025-07-27 17:18:57 -07:00
Alexander Rose
86da258280 Merge branch 'master' of https://github.com/molstar/molstar into scene-scale 2025-07-26 17:42:54 -07:00
Alexander Rose
477a80d1ca fix post-processing params hideIf logic 2025-07-26 17:40:14 -07:00
Alexander Rose
86b68018a9 add scene scaling support 2025-07-26 17:39:44 -07:00
Alexander Rose
da095d6ef9 handling move/dragt before resolving pickData breaks ray-picking 2025-07-26 16:46:08 -07:00
Alexander Rose
dc304b9e08 Merge pull request #1583 from molstar/fix-async-buffer
fix async buffer issues
2025-07-26 16:22:43 -07:00
Alexander Rose
c905fa17c4 tweak 2025-07-26 16:22:33 -07:00
Alexander Rose
a06c64e8e0 Merge pull request #1584 from molstar/pp-switch
add `enable` param for post-processing effects
2025-07-26 16:05:25 -07:00
Alexander Rose
f5441290dd Merge pull request #1567 from giagitom/box3d-spec
add tests for box3D nearestIntersectionWithRay3D
2025-07-26 16:04:59 -07:00
Alexander Rose
9f23124317 move ray box intersection code to Ray3D 2025-07-26 15:59:40 -07:00
dsehnal
8299cd638c tweaks 2025-07-26 20:37:42 +02:00
Alexander Rose
50cb08e74d Merge branch 'master' of https://github.com/molstar/molstar into fix-async-buffer 2025-07-26 07:55:21 -07:00
Alexander Rose
89552652ba Merge branch 'master' of https://github.com/molstar/molstar into pp-switch 2025-07-26 07:55:02 -07:00
Alexander Rose
37ce577813 fix text shader 2025-07-26 07:54:28 -07:00
Alexander Rose
4d9a003141 add enable param for post-processing effects
- If false, no effects are applied.
2025-07-26 07:45:59 -07:00
Alexander Rose
6f0311a53f fix async buffer issues
- mark pick-helper dirty when async pick failed
- add pixel-pack buffer wrapper
- recover pixel-pack buffer after context loss (pick buffer, hi-z pass)
2025-07-26 07:30:54 -07:00
Alexander Rose
bfd2d6b055 text shader: head rotation tweak 2025-07-26 07:23:49 -07:00
Alexander Rose
3072e60709 Merge pull request #1582 from molstar/revert-1581-fix-async-identify
Revert "fix async identify"
2025-07-26 07:22:33 -07:00
Alexander Rose
62ed8d10e3 Revert "fix async identify (#1581)"
This reverts commit 13d3c34864.
2025-07-26 07:22:12 -07:00
David Sehnal
13d3c34864 fix async identify (#1581) 2025-07-25 18:42:29 +02:00
David Sehnal
cac433efca MVS Stories: Add "Download MVS State" link (#1580) 2025-07-25 14:31:07 +02:00
dsehnal
b25ffe7151 Canvas3dInteractionHelper fix 2025-07-25 10:53:02 +02:00
David Sehnal
31074dc74c fix inv het rotation uniform (#1578) 2025-07-25 10:34:13 +02:00
giagitom
c98c01a076 fix names 2025-07-23 18:24:56 +02:00
giagitom
8966fc9396 refactor 2025-07-23 18:23:12 +02:00
dsehnal
fdbdc551e8 fix web component syntax 2025-07-22 15:25:44 +02:00
dsehnal
bb232ac3a4 pass format in mvs stories app 2025-07-22 14:37:51 +02:00
giagitom
735c25ef8d Merge branch 'master' of https://github.com/molstar/molstar into box3d-spec 2025-07-22 14:13:07 +02:00
Alexander Rose
298043313a head rotation handling tweaks 2025-07-20 22:07:37 -07:00
Alexander Rose
77cd181b91 add addCylinderFromRay3D helper function 2025-07-20 09:02:31 -07:00
Alexander Rose
b5bee042e8 add groupCount argument to Shape.create 2025-07-20 08:48:43 -07:00
Alexander Rose
4faf17ddc7 Merge pull request #1576 from molstar/headrotation
Add head rotation support
2025-07-20 08:45:15 -07:00
Alexander Rose
28774b2277 fix scale issues in cylinders & spheres shaders 2025-07-19 16:13:48 -07:00
Alexander Rose
6a7444f44e add head rotation support
- handle skybox
- handle sphere & text billboards
2025-07-19 16:13:00 -07:00
Alexander Rose
15bfa8416a Merge pull request #1575 from molstar/async-ray-picking
add async & ray picking
2025-07-19 15:16:01 -07:00
Alexander Rose
e6895ec833 cleanup, simplify AsyncPickData 2025-07-19 15:08:46 -07:00
Alexander Rose
2099ad728a add async & ray picking 2025-07-19 09:13:49 -07:00
giagitom
6fc04c3294 add tests for box3D nearestIntersectionWithRay3D 2025-07-07 18:46:06 +02:00
60 changed files with 1383 additions and 3264 deletions

View File

@@ -5,12 +5,13 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- [Breaking] Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `nearestIntersectionWithRay3D` (use `Ray3D`)
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `Ray3D.intersectBox3D`
- [Breaking] `Plane3D.distanceToSpher3D` -> `distanceToSphere3D` (fix spelling)
- [Breaking] fix typo `MarchinCubes` -> `MarchingCubes`
- [Breaking] `PluginContext.initViewer/initContainer/mount` are now async and have been renamed to include `Async` postfix
- [Breaking] Add `Volume.instances` support and a `VolumeInstances` transform to dynamically assign it
- This change is breaking because all volume objects require the `instances` field now.
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
- Update production build to use `esbuild`
- Emit explicit paths in `import`s in `lib/`
- Fix outlines on opaque elements using illumination mode
@@ -49,7 +50,18 @@ Note that since we don't clearly distinguish between a public and private interf
- Add `Ray3D` object and helpers
- Volume slice representation: add `relativeX/Y/Z` options for dimension
- Add `StructureInstances` transform
- Add `story-id` URL arg support to `mvs-stories` app
- `mvs-stories` app
- Add `story-id` URL arg support
- Add "Download MVS State" link
- Add ray-based picking
- Render narrow view of scene scene from ray origin & direction to a few pixel sized viewport
- Cast ray on every input as opposed to the standard "whole screen" picking
- Can be enabled with new `Canvas3dInteractionHelperParams.convertCoordsToRay` param
- Allows to have input methods that are 3D pointers in the scene
- Add `ray: Ray3D` property to `DragInput`, `ClickInput`, and `MoveInput`
- Add async, non-blocking picking (only WebGL2)
- Refactor `Canvas3dInteractionHelper` internals to use async picking for move events
- Add `enable` param for post-processing effects. If false, no effects are applied.
## [v4.18.0] - 2025-06-08
- MolViewSpec extension:

2768
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.0.0-dev.0",
"version": "5.0.0-dev.1",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {

View File

@@ -16,6 +16,7 @@ export class MVSStoriesContext {
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
state = {
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
currentStoryData: new BehaviorSubject<string | Uint8Array | undefined>(undefined),
isLoading: new BehaviorSubject(false),
};
@@ -27,7 +28,7 @@ export class MVSStoriesContext {
}
}
export function getMVSStoriesContext(options?: { name?: string, container?: object }) {
export function getMVSStoriesContext(options?: { name?: string, container?: object }): MVSStoriesContext {
const container: any = options?.container ?? window;
container.componentContexts ??= {};
const name = options?.name ?? '<default>';

View File

@@ -6,6 +6,8 @@
import { MolViewSpec } from '../../../extensions/mvs/behavior';
import { loadMVSData } from '../../../extensions/mvs/components/formats';
import { MVSData } from '../../../extensions/mvs/mvs-data';
import { StringLike } from '../../../mol-io/common/string-like';
import { PluginComponent } from '../../../mol-plugin-state/component';
import { createPluginUI } from '../../../mol-plugin-ui';
import { renderReact18 } from '../../../mol-plugin-ui/react18';
@@ -56,11 +58,17 @@ export class MVSStoriesViewerModel extends PluginComponent {
try {
this.context.state.isLoading.next(true);
if (cmd.kind === 'load-mvs') {
let loadedData: MVSData | StringLike | Uint8Array | undefined;
if (cmd.url) {
const data = await this.plugin.runTask(this.plugin.fetch({ url: cmd.url, type: cmd.format === 'mvsx' ? 'binary' : 'string' }));
await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
loadedData = await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
} else if (cmd.data) {
await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
loadedData = await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
}
if (StringLike.is(loadedData) || loadedData instanceof Uint8Array) {
this.context.state.currentStoryData.next(loadedData as string | Uint8Array);
} else if (loadedData) {
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
}
}
} catch (e) {

View File

@@ -38,6 +38,21 @@
gap: 16px;
}
#links {
position: absolute;
bottom: 4px;
right: 8px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.6rem;
z-index: -1;
color: #666;
}
#links a {
color: #666;
text-decoration: none;
}
@media (orientation:portrait) {
#viewer {
position: absolute;
@@ -68,10 +83,14 @@
<body>
<!-- the context-name parameter is optional and useful when embedding multiple stories in a single page -->
<div id="viewer">
<mvs-stories-viewer context-name="story1" />
<mvs-stories-viewer context-name="story1" ></mvs-stories-viewer>
</div>
<div id="controls">
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" />
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" ></mvs-stories-snapshot-markdown>
</div>
<div id="links">
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
<script>
@@ -86,10 +105,15 @@
// }
if (storyId) {
mvsStories.loadFromID(storyId, { contextName: 'story1' });
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
} else if (storyUrl) {
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
}
document.getElementById('mvs-data').addEventListener('click', (e) => {
e.preventDefault();
mvsStories.downloadCurrentStory({ contextName: 'story1' });
});
</script>
<!-- __MOLSTAR_ANALYTICS__ -->
</body>

View File

@@ -7,6 +7,7 @@
import { getMVSStoriesContext } from './context';
import './elements';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { download } from '../../mol-util/download';
import './favicon.ico';
import '../../mol-plugin-ui/skin/light.scss';
@@ -48,4 +49,16 @@ export function loadFromID(id: string, options?: { format?: 'mvsx' | 'mvsj', con
);
}
export function downloadCurrentStory(options?: { contextName?: string, filename?: string }) {
const story = getContext(options?.contextName).state.currentStoryData.value;
if (!story) return;
const isMVSJ = typeof story === 'string';
const filename = `${options?.filename ?? 'story'}.${isMVSJ ? 'mvsj' : 'mvsx'}`;
download(
new Blob([typeof story === 'string' ? story : story.buffer], { type: isMVSJ ? 'application/json' : 'application/octet-stream' }),
filename
);
};
export { MVSData };

View File

@@ -98,14 +98,14 @@
<body>
<div id="viewer">
<mvs-stories-viewer />
<mvs-stories-viewer></mvs-stories-viewer>
</div>
<div id="controls">
<div id="select-story" class="select-story"></div>
<mvs-stories-snapshot-markdown style="flex-grow: 1;" />
<mvs-stories-snapshot-markdown style="flex-grow: 1;"></mvs-stories-snapshot-markdown>
</div>
<div id="links">
<a href="#" id="mvs-data" filename="kinase-story.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
<script>

View File

@@ -166,6 +166,8 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
} else {
throw new Error(`Unknown MolViewSpec format: ${format}`);
}
return data;
}
function clearMVSXFileAssets(plugin: PluginContext) {

View File

@@ -6,7 +6,7 @@
*/
import * as iots from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { PathReporter } from "io-ts/lib/PathReporter";
import { onelinerJsonString } from '../../../../mol-util/json';

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
@@ -11,6 +11,7 @@ import { CameraTransitionManager } from './camera/transition';
import { BehaviorSubject } from 'rxjs';
import { Scene } from '../mol-gl/scene';
import { assertUnreachable } from '../mol-util/type-helpers';
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
export type { ICamera };
@@ -26,6 +27,7 @@ interface ICamera {
readonly near: number,
readonly fogFar: number,
readonly fogNear: number,
readonly headRotation: Mat4,
}
const tmpClip = Vec4();
@@ -35,6 +37,7 @@ export class Camera implements ICamera {
readonly projection: Mat4 = Mat4.identity();
readonly projectionView: Mat4 = Mat4.identity();
readonly inverseProjectionView: Mat4 = Mat4.identity();
readonly headRotation: Mat4 = Mat4.zero();
readonly viewport: Viewport;
readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
@@ -69,7 +72,7 @@ export class Camera implements ICamera {
return false;
}
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target) * this.state.scale;
this.zoom = this.viewport.height / height;
updateClip(this);
@@ -191,6 +194,22 @@ export class Camera implements ICamera {
return (2 / w) / (rx * Math.abs(P00));
}
getRay(out: Ray3D, x: number, y: number) {
if (this.state.mode === 'orthographic') {
Vec3.set(out.origin, x, y, 0);
this.unproject(out.origin, out.origin);
Vec3.normalize(out.direction, Vec3.sub(out.direction, this.target, this.position));
Vec3.scaleAndAdd(out.origin, out.origin, out.direction, -this.near);
} else {
Vec3.copy(out.origin, this.state.position);
Vec3.scale(out.origin, out.origin, this.state.scale);
Vec3.set(out.direction, x, y, 0.5);
this.unproject(out.direction, out.direction);
Vec3.normalize(out.direction, Vec3.sub(out.direction, out.direction, out.origin));
}
return out;
}
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
this.viewport = viewport;
Camera.copySnapshot(this.state, state);
@@ -270,6 +289,8 @@ export namespace Camera {
clipFar: true,
minNear: 5,
minFar: 0,
scale: 1,
};
}
@@ -287,6 +308,8 @@ export namespace Camera {
clipFar: boolean
minNear: number
minFar: number
scale: number
}
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -306,6 +329,8 @@ export namespace Camera {
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
if (typeof source.scale !== 'undefined') out.scale = source.scale;
return out;
}
@@ -318,12 +343,26 @@ export namespace Camera {
&& a.clipFar === b.clipFar
&& a.minNear === b.minNear
&& a.minFar === b.minFar
&& a.scale === b.scale
&& Vec3.exactEquals(a.position, b.position)
&& Vec3.exactEquals(a.up, b.up)
&& Vec3.exactEquals(a.target, b.target);
}
}
const tmpPosition = Vec3();
const tmpTarget = Vec3();
function updateView(camera: Camera) {
if (camera.state.scale === 1) {
Mat4.lookAt(camera.view, camera.state.position, camera.state.target, camera.state.up);
} else {
Vec3.scale(tmpPosition, camera.state.position, camera.state.scale);
Vec3.scale(tmpTarget, camera.state.target, camera.state.scale);
Mat4.lookAt(camera.view, tmpPosition, tmpTarget, camera.state.up);
}
}
function updateOrtho(camera: Camera) {
const { viewport, zoom, near, far, viewOffset } = camera;
@@ -357,7 +396,7 @@ function updateOrtho(camera: Camera) {
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
// build view matrix
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
updateView(camera);
}
function updatePers(camera: Camera) {
@@ -381,15 +420,23 @@ function updatePers(camera: Camera) {
Mat4.perspective(camera.projection, left, left + width, top, top - height, near, far);
// build view matrix
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
updateView(camera);
}
function updateClip(camera: Camera) {
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
if (radius < 0.01) radius = 0.01;
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar, scale } = camera.state;
radiusMax *= scale;
minFar *= scale;
minNear *= scale;
radius *= scale;
const minRadius = 0.01 * scale;
if (radius < minRadius) radius = minRadius;
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
const cameraDistance = Vec3.distance(camera.position, camera.target);
Vec3.scale(tmpTarget, camera.state.target, scale);
Vec3.scale(tmpPosition, camera.state.position, scale);
const cameraDistance = Vec3.distance(tmpPosition, tmpTarget);
let near = cameraDistance - radius;
let far = cameraDistance + normalizedFar;
@@ -405,7 +452,7 @@ function updateClip(camera: Camera) {
if (near === far) {
// make sure near and far are not identical to avoid Infinity in the projection matrix
far = near + 0.01;
far = near + 0.01 * scale;
}
const fogNearFactor = -(50 - fog) / 50;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2025 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>
@@ -61,6 +61,7 @@ class EyeCamera implements ICamera {
projection = Mat4();
projectionView = Mat4();
inverseProjectionView = Mat4();
headRotation = Mat4();
state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
far: number = 0;
@@ -69,30 +70,29 @@ class EyeCamera implements ICamera {
fogNear: number = 0;
}
const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity();
const tmpEyeLeft = Mat4.identity();
const tmpEyeRight = Mat4.identity();
function copyStates(parent: Camera, eye: EyeCamera) {
Viewport.copy(eye.viewport, parent.viewport);
Mat4.copy(eye.view, parent.view);
Mat4.copy(eye.projection, parent.projection);
Mat4.copy(eye.headRotation, parent.headRotation);
Camera.copySnapshot(eye.state, parent.state);
Camera.copyViewOffset(eye.viewOffset, parent.viewOffset);
eye.far = parent.far;
eye.near = parent.near;
eye.fogFar = parent.fogFar;
eye.fogNear = parent.fogNear;
}
//
function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) {
// Copy the states
Viewport.copy(left.viewport, camera.viewport);
Mat4.copy(left.view, camera.view);
Mat4.copy(left.projection, camera.projection);
Camera.copySnapshot(left.state, camera.state);
Camera.copyViewOffset(left.viewOffset, camera.viewOffset);
left.far = camera.far;
left.near = camera.near;
left.fogFar = camera.fogFar;
left.fogNear = camera.fogNear;
Viewport.copy(right.viewport, camera.viewport);
Mat4.copy(right.view, camera.view);
Mat4.copy(right.projection, camera.projection);
Camera.copySnapshot(right.state, camera.state);
Camera.copyViewOffset(right.viewOffset, camera.viewOffset);
right.far = camera.far;
right.near = camera.near;
right.fogFar = camera.fogFar;
right.fogNear = camera.fogNear;
copyStates(camera, left);
copyStates(camera, right);
// update the view offsets
@@ -112,8 +112,8 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
// translate xOffset
eyeLeft[12] = -eyeSepHalf;
eyeRight[12] = eyeSepHalf;
tmpEyeLeft[12] = -eyeSepHalf;
tmpEyeRight[12] = eyeSepHalf;
// for left eye
@@ -123,7 +123,7 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
left.projection[0] = 2 * camera.near / (xmax - xmin);
left.projection[8] = (xmax + xmin) / (xmax - xmin);
Mat4.mul(left.view, left.view, eyeLeft);
Mat4.mul(left.view, left.view, tmpEyeLeft);
Mat4.mul(left.projectionView, left.projection, left.view);
Mat4.invert(left.inverseProjectionView, left.projectionView);
@@ -135,7 +135,7 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
right.projection[0] = 2 * camera.near / (xmax - xmin);
right.projection[8] = (xmax + xmin) / (xmax - xmin);
Mat4.mul(right.view, right.view, eyeRight);
Mat4.mul(right.view, right.view, tmpEyeRight);
Mat4.mul(right.projectionView, right.projection, right.view);
Mat4.invert(right.inverseProjectionView, right.projectionView);
}

View File

@@ -28,8 +28,8 @@ import { SetUtils } from '../mol-util/set';
import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
import { PostprocessingParams } from './passes/postprocessing';
import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
import { PickData } from './passes/pick';
import { PickHelper } from './passes/pick';
import { AsyncPickData, DefaultPickOptions, PickData } from './passes/pick';
import { PickHelper } from './helper/pick-helper';
import { ImagePass, ImageProps } from './passes/image';
import { Sphere3D } from '../mol-math/geometry';
import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
@@ -47,6 +47,8 @@ import { deepClone } from '../mol-util/object';
import { HiZParams, HiZPass } from './passes/hi-z';
import { IlluminationParams } from './passes/illumination';
import { isMobileBrowser } from '../mol-util/browser';
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
import { RayHelper } from './helper/ray-helper';
export const Canvas3DParams = {
camera: PD.Group({
@@ -57,6 +59,7 @@ export const Canvas3DParams = {
off: PD.Group({})
}, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
scale: PD.Numeric(1, { min: 0.001, max: 1, step: 0.001 }, { label: 'Scene scale' }),
manualReset: PD.Boolean(false, { isHidden: true }),
}, { pivot: 'mode' }),
cameraFog: PD.MappedStatic('on', {
@@ -330,7 +333,8 @@ interface Canvas3D {
pause(noDraw?: boolean): void
/** Sets drawPaused = false without starting the built in animation loop */
resume(): void
identify(x: number, y: number): PickData | undefined
identify(target: Vec2 | Ray3D): PickData | undefined
asyncIdentify(target: Vec2 | Ray3D): AsyncPickData | undefined
mark(loci: Representation.Loci, action: MarkerAction): void
getLoci(pickingId: PickingId | undefined): Representation.Loci
@@ -412,6 +416,7 @@ namespace Canvas3D {
clipFar: p.cameraClipping.far,
minNear: p.cameraClipping.minNear,
fov: degToRad(p.camera.fov),
scale: p.camera.scale,
}, { x, y, width, height });
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
@@ -422,8 +427,13 @@ namespace Canvas3D {
const renderer = Renderer.create(webgl, p.renderer);
renderer.setOcclusionTest(hiZ.isOccluded);
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, p.pickPadding);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
const pickOptions = {
pickPadding: p.pickPadding,
maxAsyncReadLag: DefaultPickOptions.maxAsyncReadLag,
};
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, pickOptions);
const rayHelper = new RayHelper(webgl, renderer, scene, helper, pickOptions);
const interactionHelper = new Canvas3dInteractionHelper(identify, asyncIdentify, getLoci, input, camera, controls, p.interaction);
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
@@ -649,9 +659,26 @@ namespace Canvas3D {
animationFrameHandle = 0;
}
function identify(x: number, y: number): PickData | undefined {
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
function identify(target: Vec2 | Ray3D): PickData | undefined {
if (webgl.isContextLost) return undefined;
if ('origin' in target) {
return rayHelper.identify(target, camera);
} else {
const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera;
return pickHelper.identify(target[0], target[1], cam);
}
}
function asyncIdentify(target: Vec2 | Ray3D): AsyncPickData | undefined {
if (webgl.isContextLost) return undefined;
if ('origin' in target) {
return rayHelper.asyncIdentify(target, camera);
} else {
const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera;
return pickHelper.asyncIdentify(target[0], target[1], cam);
}
}
function commit(isSynchronous: boolean = false) {
@@ -841,6 +868,7 @@ namespace Canvas3D {
helper: { ...helper.camera.props },
stereo: { ...p.camera.stereo },
fov: Math.round(radToDeg(camera.state.fov)),
scale: camera.state.scale,
manualReset: !!p.camera.manualReset
},
cameraFog: camera.state.fog > 0
@@ -875,6 +903,10 @@ namespace Canvas3D {
});
const contextRestoredSub = contextRestored.subscribe(() => {
pickHelper.reset();
rayHelper.reset();
hiZ.reset();
scene.forEach(r => {
if (r.values.meta?.ref.value.reset) {
r.values.meta.ref.value.reset();
@@ -952,7 +984,7 @@ namespace Canvas3D {
input.click.subscribe(e => {
if (!e.modifiers.control || e.button !== 2) return;
const p = identify(e.x, e.y);
const p = identify(Vec2.create(e.x, e.y));
if (!p) {
occlusionLoci = undefined;
printOcclusion(occlusionLoci);
@@ -1020,6 +1052,7 @@ namespace Canvas3D {
pause,
resume: () => { drawPaused = false; },
identify,
asyncIdentify,
mark,
getLoci,
@@ -1060,6 +1093,9 @@ namespace Canvas3D {
if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
cameraState.fov = degToRad(props.camera.fov);
}
if (props.camera && props.camera.scale !== undefined && props.camera.scale !== cameraState.scale) {
cameraState.scale = props.camera.scale;
}
if (props.cameraFog !== undefined && props.cameraFog.params) {
const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
if (newFog !== camera.state.fog) cameraState.fog = newFog;
@@ -1170,6 +1206,9 @@ namespace Canvas3D {
renderer.dispose();
interactionHelper.dispose();
hiZ.dispose();
pickHelper.dispose();
rayHelper.dispose();
if (fenceSync !== null) {
webgl.deleteSync(fenceSync);
fenceSync = null;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
@@ -14,14 +14,14 @@ import { Camera } from '../camera';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Bond } from '../../mol-model/structure';
import { TrackballControls } from '../controls/trackball';
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d';
import { AsyncPickData } from '../passes/pick';
type Canvas3D = import('../canvas3d').Canvas3D
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
type DragEvent = import('../canvas3d').Canvas3D.DragEvent
type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
enum InputEvent { Move, Click, Drag }
const tmpPosA = Vec3();
const tmpPos = Vec3();
const tmpNorm = Vec3();
@@ -29,6 +29,7 @@ const tmpNorm = Vec3();
export const Canvas3dInteractionHelperParams = {
maxFps: PD.Numeric(30, { min: 10, max: 60, step: 10 }),
preferAtomPixelPadding: PD.Numeric(3, { min: 0, max: 20, step: 1 }, { description: 'Number of extra pixels at which to prefer atoms over bonds.' }),
convertCoordsToRay: PD.Boolean(false, { description: 'Convert screen coordinates to ray for picking.' }),
};
export type Canvas3dInteractionHelperParams = typeof Canvas3dInteractionHelperParams
export type Canvas3dInteractionHelperProps = PD.Values<Canvas3dInteractionHelperParams>
@@ -47,10 +48,11 @@ export class Canvas3dInteractionHelper {
private endX = -1;
private endY = -1;
private id: PickingId | undefined = void 0;
private ray: Ray3D | undefined = void 0;
private pickData: AsyncPickData | undefined = void 0;
private position: Vec3 | undefined = void 0;
private currentIdentifyT = 0;
private isInteracting = false;
private prevLoci: Representation.Loci = Representation.Loci.Empty;
@@ -68,46 +70,66 @@ export class Canvas3dInteractionHelper {
Object.assign(this.props, props);
}
private identify(e: InputEvent, t: number) {
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)) {
this.events.drag.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, pageStart: Vec2.create(this.startX, this.startY), pageEnd: Vec2.create(this.endX, this.endY) });
this.startX = this.endX;
this.startY = this.endY;
}
return;
private getTarget(): Vec2 | Ray3D {
if (this.ray) {
return this.ray;
} else if (this.props.convertCoordsToRay) {
return this.camera.getRay(Ray3D(), this.endX, this.input.height - this.endY);
} else {
return Vec2.create(this.endX, this.endY);
}
}
private handleMove() {
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
if (xyChanged) {
const pickData = this.canvasIdentify(this.endX, this.endY);
this.id = pickData?.id;
this.position = pickData?.position;
this.pickData = this.canvasAsyncIdentify(this.getTarget());
this.startX = this.endX;
this.startY = this.endY;
}
}
if (e === InputEvent.Click) {
const loci = this.getLoci(this.id, this.position);
this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
this.prevLoci = loci;
return;
}
if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
const loci = this.getLoci(this.id, this.position);
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
private handleClick() {
const pickData = this.canvasIdentify(this.getTarget());
const loci = this.getLoci(pickData?.id, pickData?.position);
this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: pickData?.position });
this.prevLoci = loci;
}
private handleDrag() {
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
if (xyChanged && !this.outsideViewport(this.startX, this.startY, this.ray)) {
this.events.drag.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, pageStart: Vec2.create(this.startX, this.startY), pageEnd: Vec2.create(this.endX, this.endY) });
this.startX = this.endX;
this.startY = this.endY;
}
}
tick(t: number) {
if (this.inside && t - this.prevT > 1000 / this.props.maxFps) {
if (!this.inside) return;
if (this.pickData) {
const pickData = this.pickData.tryGet();
if (pickData !== 'pending') {
this.position = pickData?.position;
if (this.inside) {
const loci = this.getLoci(pickData?.id, pickData?.position);
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: pickData?.position });
this.prevLoci = loci;
}
this.pickData = undefined;
}
}
if (t - this.prevT > 1000 / this.props.maxFps) {
this.prevT = t;
this.currentIdentifyT = t;
this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t);
if (this.isInteracting) {
this.handleDrag();
} else {
this.handleMove();
}
}
}
@@ -119,22 +141,24 @@ export class Canvas3dInteractionHelper {
}
}
private move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
private move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, ray?: Ray3D) {
this.inside = true;
this.buttons = buttons;
this.button = button;
this.modifiers = modifiers;
this.ray = ray;
this.endX = x;
this.endY = y;
}
private click(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
private click(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, ray?: Ray3D) {
this.endX = x;
this.endY = y;
this.buttons = buttons;
this.button = button;
this.modifiers = modifiers;
this.identify(InputEvent.Click, 0);
this.ray = ray;
this.handleClick();
}
private drag(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
@@ -143,7 +167,7 @@ export class Canvas3dInteractionHelper {
this.buttons = buttons;
this.button = button;
this.modifiers = modifiers;
this.identify(InputEvent.Drag, 0);
this.handleDrag();
}
private modify(modifiers: ModifiersKeys) {
@@ -152,7 +176,9 @@ export class Canvas3dInteractionHelper {
this.events.hover.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
}
private outsideViewport(x: number, y: number) {
private outsideViewport(x: number, y: number, ray?: Ray3D) {
if (ray) return false;
const { input, camera: { viewport } } = this;
x *= input.pixelRatio;
y *= input.pixelRatio;
@@ -189,7 +215,7 @@ export class Canvas3dInteractionHelper {
this.ev.dispose();
}
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
constructor(private canvasIdentify: Canvas3D['identify'], private canvasAsyncIdentify: Canvas3D['asyncIdentify'], 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 }) => {
@@ -198,14 +224,14 @@ export class Canvas3dInteractionHelper {
this.drag(x, y, buttons, button, modifiers);
});
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement, ray }) => {
if (!inside || this.isInteracting) return;
if (!onElement) {
this.leave();
return;
}
// console.log('move');
this.move(x, y, buttons, button, modifiers);
this.move(x, y, buttons, button, modifiers, ray);
});
input.leave.subscribe(() => {
@@ -213,10 +239,10 @@ export class Canvas3dInteractionHelper {
this.leave();
});
input.click.subscribe(({ x, y, buttons, button, modifiers }) => {
if (this.outsideViewport(x, y)) return;
input.click.subscribe(({ x, y, buttons, button, modifiers, ray }) => {
if (this.outsideViewport(x, y, ray)) return;
// console.log('click');
this.click(x, y, buttons, button, modifiers);
this.click(x, y, buttons, button, modifiers, ray);
});
input.interactionEnd.subscribe(() => {

View File

@@ -0,0 +1,208 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Renderer } from '../../mol-gl/renderer';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { spiral2d } from '../../mol-math/misc';
import { isTimingMode } from '../../mol-util/debug';
import { Camera } from '../camera';
import { StereoCamera } from '../camera/stereo';
import { cameraUnproject, Viewport } from '../camera/util';
import { Helper } from '../helper/helper';
import { AsyncPickData, AsyncPickStatus, checkAsyncPickingSupport, PickBuffers, PickData, PickOptions, PickPass } from '../passes/pick';
export class PickHelper {
dirty = true;
private pickPadding: number;
private buffers = new PickBuffers(this.webgl, this.pickPass);
private viewport = Viewport();
private pickRatio: number;
private pickX: number;
private pickY: number;
private pickWidth: number;
private pickHeight: number;
private halfPickWidth: number;
private spiral: [number, number][];
setViewport(x: number, y: number, width: number, height: number) {
Viewport.set(this.viewport, x, y, width, height);
this.update();
}
setPickPadding(pickPadding: number) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
private update() {
const { x, y, width, height } = this.viewport;
this.pickRatio = this.pickPass.pickRatio;
this.pickX = Math.ceil(x * this.pickRatio);
this.pickY = Math.ceil(y * this.pickRatio);
const pickWidth = Math.floor(width * this.pickRatio);
const pickHeight = Math.floor(height * this.pickRatio);
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
this.pickWidth = pickWidth;
this.pickHeight = pickHeight;
this.halfPickWidth = Math.floor(this.pickWidth / 2);
this.buffers.setViewport(this.pickX, this.pickY, this.pickWidth, this.pickHeight);
}
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
this.dirty = true;
}
private render(camera: Camera | StereoCamera) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
renderer.setTransparentBackground(false);
renderer.setDrawingBufferSize(pickWidth, pickHeight);
renderer.setPixelRatio(this.pickRatio);
if (StereoCamera.is(camera)) {
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.left, scene, helper);
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.right, scene, helper);
} else {
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
this.pickPass.render(renderer, camera, scene, helper);
}
this.dirty = false;
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.render');
}
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
if (this.webgl.isContextLost) return;
const { webgl, pickRatio } = this;
if (webgl.isContextLost) return;
x *= webgl.pixelRatio;
y *= webgl.pixelRatio;
y = this.pickPass.drawingBufferHeight - y; // flip y
const { viewport } = this;
// check if within viewport
if (x < viewport.x ||
y < viewport.y ||
x > viewport.x + viewport.width ||
y > viewport.y + viewport.height
) return;
const xv = x - viewport.x;
const yv = y - viewport.y;
const xp = Math.floor(xv * pickRatio);
const yp = Math.floor(yv * pickRatio);
const pickingId = this.buffers.getPickingId(xp, yp);
if (pickingId === undefined) return;
const z = this.buffers.getDepth(xp, yp);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {
position[0] = viewport.x + (xv - halfWidth) * 2;
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
} else {
position[0] = viewport.x + xv * 2;
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
}
} else {
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
}
return { id: pickingId, position };
}
private prepare() {
if (this.pickRatio !== this.pickPass.pickRatio) {
this.update();
}
}
private getPickData(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
for (const d of this.spiral) {
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
if (pickData) return pickData;
}
}
identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
this.prepare();
if (this.dirty) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
this.render(camera);
this.buffers.read();
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');
}
return this.getPickData(x, y, camera);
}
asyncIdentify(x: number, y: number, camera: Camera | StereoCamera): AsyncPickData | undefined {
this.prepare();
if (this.dirty) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.asyncIdentify');
this.render(camera);
this.buffers.asyncRead();
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.asyncIdentify');
}
return {
tryGet: () => {
const status = this.buffers.check();
if (status === AsyncPickStatus.Resolved) {
return this.getPickData(x, y, camera);
} else if (status === AsyncPickStatus.Pending) {
return 'pending';
} else if (status === AsyncPickStatus.Failed) {
this.dirty = true;
}
}
};
}
reset() {
this.buffers.reset();
this.dirty = true;
}
dispose() {
this.buffers.dispose();
}
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, options: PickOptions) {
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
this.pickPadding = options.pickPadding;
if (!checkAsyncPickingSupport(webgl)) {
this.asyncIdentify = (x, y, camera) => ({
tryGet: () => this.identify(x, y, camera)
});
}
}
}

View File

@@ -0,0 +1,205 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Renderer } from '../../mol-gl/renderer';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d';
import { Mat4, Quat, Vec3 } from '../../mol-math/linear-algebra';
import { degToRad, spiral2d } from '../../mol-math/misc';
import { isTimingMode } from '../../mol-util/debug';
import { Camera } from '../camera';
import { cameraUnproject } from '../camera/util';
import { Viewport } from '../camera/util';
import { Helper } from './helper';
import { AsyncPickData, PickBuffers, PickData, PickPass, PickOptions, checkAsyncPickingSupport, AsyncPickStatus } from '../passes/pick';
import { Sphere3D } from '../../mol-math/geometry/primitives/sphere3d';
export class RayHelper {
private viewport = Viewport();
private size: number;
private spiral: [number, number][];
private pickPadding: number;
private camera: Camera;
private pickPass: PickPass;
private buffers: PickBuffers;
setPickPadding(pickPadding: number) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
private update() {
const size = this.pickPadding * 2 + 1;
Viewport.set(this.viewport, 0, 0, size, size);
this.buffers.setViewport(0, 0, size, size);
this.spiral = spiral2d(this.pickPadding);
this.size = size;
this.pickPass.setSize(size, size);
}
private render(camera: Camera) {
if (isTimingMode) this.webgl.timer.mark('RayHelper.render', { captureStats: true });
const { renderer, scene, helper } = this;
renderer.setTransparentBackground(false);
renderer.setDrawingBufferSize(this.size, this.size);
renderer.setPixelRatio(1);
renderer.setViewport(0, 0, this.size, this.size);
this.pickPass.render(renderer, camera, scene, helper);
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.render');
}
private identifyInternal(x: number, y: number): PickData | undefined {
if (this.webgl.isContextLost) return;
const { viewport } = this;
const pickingId = this.buffers.getPickingId(x, y);
if (pickingId === undefined) return;
const z = this.buffers.getDepth(x, y);
const position = Vec3.create(x, y, z);
cameraUnproject(position, position, viewport, this.camera.inverseProjectionView);
return { id: pickingId, position };
}
private prepare(ray: Ray3D, cam: Camera) {
this.camera.far = cam.far;
this.camera.near = cam.near;
this.camera.fogFar = cam.fogFar;
this.camera.fogNear = cam.fogNear;
Viewport.copy(this.camera.viewport, this.viewport);
Camera.copySnapshot(this.camera.state, { ...cam.state, mode: 'orthographic' });
updateOrthoRayCamera(this.camera, ray);
Mat4.mul(this.camera.projectionView, this.camera.projection, this.camera.view);
Mat4.tryInvert(this.camera.inverseProjectionView, this.camera.projectionView);
}
private getPickData(): PickData | undefined {
const c = this.pickPadding;
for (const d of this.spiral) {
const pickData = this.identifyInternal(c + d[0], c + d[1]);
if (pickData) return pickData;
}
}
sphere = Sphere3D();
private intersectsScene(ray: Ray3D, scale: number): boolean {
Sphere3D.scaleNX(this.sphere, this.scene.boundingSphereVisible, scale);
return Ray3D.isInsideSphere3D(ray, this.sphere) || Ray3D.isIntersectingSphere3D(ray, this.sphere);
}
identify(ray: Ray3D, cam: Camera): PickData | undefined {
if (!this.intersectsScene(ray, cam.state.scale)) return;
this.prepare(ray, cam);
if (isTimingMode) this.webgl.timer.mark('RayHelper.identify');
this.render(this.camera);
this.buffers.read();
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.identify');
return this.getPickData();
}
asyncIdentify(ray: Ray3D, cam: Camera): AsyncPickData | undefined {
if (!this.intersectsScene(ray, cam.state.scale)) return;
this.prepare(ray, cam);
if (isTimingMode) this.webgl.timer.mark('RayHelper.asyncIdentify');
this.render(this.camera);
this.buffers.asyncRead();
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.asyncIdentify');
return {
tryGet: () => {
const status = this.buffers.check();
if (status === AsyncPickStatus.Resolved) {
return this.getPickData();
} else if (status === AsyncPickStatus.Pending) {
return 'pending';
}
}
};
}
reset() {
this.buffers.reset();
this.pickPass.reset();
}
dispose() {
this.buffers.dispose();
this.pickPass.dispose();
}
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, options: PickOptions) {
const size = options.pickPadding * 2 + 1;
this.camera = new Camera();
this.pickPass = new PickPass(webgl, size, size, 1);
this.buffers = new PickBuffers(this.webgl, this.pickPass, options.maxAsyncReadLag);
this.pickPadding = options.pickPadding;
this.update();
if (!checkAsyncPickingSupport(webgl)) {
this.asyncIdentify = (ray, cam) => ({
tryGet: () => this.identify(ray, cam)
});
}
}
}
//
function updateOrthoRayCamera(camera: Camera, ray: Ray3D) {
const { near, far, viewport } = camera;
const height = 2 * Math.tan(degToRad(0.1) / 2) * Vec3.distance(camera.position, camera.target) * camera.state.scale;
const zoom = viewport.height / height;
const fullLeft = -viewport.width / 2;
const fullRight = viewport.width / 2;
const fullTop = viewport.height / 2;
const fullBottom = -viewport.height / 2;
const dx = (fullRight - fullLeft) / (2 * zoom);
const dy = (fullTop - fullBottom) / (2 * zoom);
const cx = (fullRight + fullLeft) / 2;
const cy = (fullTop + fullBottom) / 2;
const left = cx - dx;
const right = cx + dx;
const top = cy + dy;
const bottom = cy - dy;
// build projection matrix
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
const direction = Vec3.normalize(Vec3(), ray.direction);
const r = Quat.fromUnitVec3(Quat(), direction, Vec3.negUnitZ);
Quat.invert(r, r);
const eye = Vec3.clone(ray.origin);
const up = Vec3.transformQuat(Vec3(), Vec3.unitY, r);
const target = Vec3.add(Vec3(), eye, direction);
// build view matrix
Mat4.lookAt(camera.view, eye, target, up);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -26,6 +26,7 @@ import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { degToRad, isPowerOfTwo } from '../../mol-math/misc';
import { Mat3 } from '../../mol-math/linear-algebra/3d/mat3';
import { Euler } from '../../mol-math/linear-algebra/3d/euler';
import { PostprocessingProps } from './postprocessing';
const SharedParams = {
opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
@@ -172,10 +173,14 @@ export class BackgroundPass {
}
const m = this.renderable.values.uViewDirectionProjectionInverse.ref.value;
Vec3.sub(this.dir, cam.state.position, cam.state.target);
Vec3.setMagnitude(this.dir, this.dir, 0.1);
Vec3.copy(this.position, this.dir);
Mat4.lookAt(m, this.position, this.target, cam.state.up);
if (Mat4.isZero(camera.headRotation)) {
Vec3.sub(this.dir, cam.state.position, cam.state.target);
Vec3.setMagnitude(this.dir, this.dir, 0.1);
Vec3.copy(this.position, this.dir);
Mat4.lookAt(m, this.position, this.target, cam.state.up);
} else {
Mat4.invert(m, camera.headRotation);
}
Mat4.mul(m, cam.projection, m);
Mat4.invert(m, m);
ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m);
@@ -292,7 +297,7 @@ export class BackgroundPass {
ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height));
}
isEnabled(props: BackgroundProps) {
private _isEnabled(props: BackgroundProps) {
return !!(
(this.skybox && this.skybox.loaded) ||
(this.image && this.image.loaded) ||
@@ -301,6 +306,10 @@ export class BackgroundPass {
);
}
isEnabled(props: PostprocessingProps) {
return props.enabled && this._isEnabled(props.background);
}
private isReady() {
return !!(
(this.skybox && this.skybox.loaded) ||
@@ -315,7 +324,7 @@ export class BackgroundPass {
clear(props: BackgroundProps, transparentBackground: boolean, backgroundColor: Color) {
const { gl, state } = this.webgl;
if (this.isEnabled(props)) {
if (this._isEnabled(props)) {
if (transparentBackground) {
state.clearColor(0, 0, 0, 0);
} else {
@@ -332,7 +341,7 @@ export class BackgroundPass {
}
render(props: BackgroundProps) {
if (!this.isEnabled(props) || !this.isReady()) return;
if (!this._isEnabled(props) || !this.isReady()) return;
if (this.renderable.values.dVariant.ref.value === 'image') {
this.updateImageScaling();

View File

@@ -38,7 +38,7 @@ export type BloomProps = PD.Values<typeof BloomParams>
export class BloomPass {
static isEnabled(props: PostprocessingProps) {
return props.bloom.name === 'on';
return props.enabled && props.bloom.name === 'on';
}
readonly emissiveTarget: RenderTarget;

View File

@@ -37,7 +37,7 @@ export type DofProps = PD.Values<typeof DofParams>
export class DofPass {
static isEnabled(props: PostprocessingProps) {
return props.dof.name !== 'off';
return props.enabled && props.dof.name !== 'off';
}
readonly target: RenderTarget;
@@ -119,18 +119,18 @@ export class DofPass {
needsUpdate = true;
}
const wolrdCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
const distance = Vec3.distance(camera.state.position, wolrdCenter);
const worldCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
const distance = Vec3.distance(camera.state.position, worldCenter);
const inFocus = distance + props.inFocus;
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus);
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus * camera.state.scale);
// transform center in view space
const center = this.renderable.values.uCenter.ref.value;
Vec3.transformMat4(center, wolrdCenter, camera.view);
Vec3.transformMat4(center, worldCenter, 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);
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM * camera.state.scale);
if (needsUpdate) {
this.renderable.update();

View File

@@ -377,6 +377,7 @@ export class DrawPass {
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const dofEnabled = DofPass.isEnabled(props.postprocessing);
const bloomEnabled = BloomPass.isEnabled(props.postprocessing);
const { x, y, width, height } = camera.viewport;
renderer.setViewport(x, y, width, height);
@@ -446,7 +447,7 @@ export class DrawPass {
needsTargetCopy = true;
}
if (props.postprocessing.dof.name === 'on') {
if (dofEnabled && props.postprocessing.dof.name === 'on') {
const input = AntialiasingPass.isEnabled(props.postprocessing)
? this.antialiasing.target.texture
: PostprocessingPass.isEnabled(props.postprocessing)
@@ -469,7 +470,7 @@ export class DrawPass {
}
}
if (props.postprocessing.bloom.name === 'on') {
if (bloomEnabled && props.postprocessing.bloom.name === 'on') {
const emissiveBloom = props.postprocessing.bloom.params.mode === 'emissive';
if (emissiveBloom && scene.emissiveAverage > 0) {
@@ -493,7 +494,7 @@ export class DrawPass {
const { renderer, camera, scene, helper } = ctx;
this.postprocessing.setTransparentBackground(props.transparentBackground);
const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing.background);
const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing);
renderer.setTransparentBackground(transparentBackground);
renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -20,7 +20,7 @@ import { Camera } from '../camera';
import { Viewport } from '../camera/util';
import { DrawPass } from './draw';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { getBuffer } from '../../mol-gl/webgl/buffer';
import { PixelPackBuffer } from '../../mol-gl/webgl/buffer';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3transformMat4 = Vec3.transformMat4;
@@ -128,7 +128,7 @@ export class HiZPass {
private readonly levelData: LevelData = [];
private readonly fb: Framebuffer;
private readonly buf: WebGLBuffer;
private readonly buf: PixelPackBuffer;
private readonly tex: Texture;
private readonly renderable: HiZRenderable;
private readonly supported: boolean;
@@ -221,10 +221,7 @@ export class HiZPass {
const hw = this.tex.getWidth();
const hh = this.tex.getHeight();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
gl.bufferData(gl.PIXEL_PACK_BUFFER, this.buffer.byteLength, gl.STREAM_READ);
gl.readPixels(0, 0, hw, hh, gl.RED, gl.FLOAT, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
this.buf.read(0, 0, hw, hh);
this.sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
@@ -249,9 +246,7 @@ export class HiZPass {
this.frameLag += 1;
// console.log(`waiting for buffer data for ${this.frameLag} frames`);
} else {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this.buffer);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
this.buf.getSubData(this.buffer);
// console.log(`got buffer data after ${this.frameLag + 1} frames`);
gl.deleteSync(this.sync);
this.sync = null;
@@ -510,6 +505,16 @@ export class HiZPass {
//
reset() {
this.sync = null;
this.ready = false;
this.frameLag = 0;
this.levelData.length = 0;
const { x, y, width, height } = this.viewport;
this.setViewport(x, y, width, height);
}
dispose() {
if (!this.supported) return;
@@ -517,7 +522,7 @@ export class HiZPass {
this.fb.destroy();
this.tex.destroy();
this.webgl.gl.deleteBuffer(this.buf);
this.buf.destroy();
this.renderable.dispose();
for (const td of this.levelData) {
@@ -527,6 +532,8 @@ export class HiZPass {
}
constructor(private webgl: WebGLContext, private drawPass: DrawPass, canvas: HTMLCanvasElement | undefined, props: Partial<HiZProps>) {
this.props = { ...PD.getDefaultValues(HiZParams), ...props };
const { gl, extensions } = webgl;
if (!isWebGL2(gl) || !extensions.colorBufferFloat) {
if (isDebugMode) {
@@ -552,8 +559,7 @@ export class HiZPass {
}
this.supported = true;
this.props = { ...PD.getDefaultValues(HiZParams), ...props };
this.buf = getBuffer(gl);
this.buf = webgl.resources.pixelPack('alpha', 'float');
this.renderable = createHiZRenderable(webgl, this.drawPass.depthTextureOpaque);
if (isDebugMode && canvas) {

View File

@@ -33,6 +33,8 @@ import { JitterVectors, MultiSampleProps } from './multi-sample';
import { compose_frag as multiSample_compose_frag } from '../../mol-gl/shader/compose.frag';
import { clamp, lerp } from '../../mol-math/interpolate';
import { SsaoProps } from './ssao';
import { OutlinePass } from './outline';
import { BloomPass } from './bloom';
type Props = {
transparentBackground: boolean;
@@ -313,8 +315,11 @@ export class IlluminationPass {
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.postprocessing.outline.name === 'on' && !props.illumination.ignoreOutline;
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
const outlinesEnabled = OutlinePass.isEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
const occlusionEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
const bloomEnabled = BloomPass.isEnabled(props.postprocessing);
const dofEnabled = DofPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
@@ -327,7 +332,7 @@ export class IlluminationPass {
ValueCell.update(this.composeRenderable.values.dOutlineEnable, outlinesEnabled);
}
if (props.postprocessing.outline.name === 'on') {
if (outlinesEnabled && props.postprocessing.outline.name === 'on') {
const { transparentOutline, outlineScale } = this.drawPass.postprocessing.outline.update(camera, props.postprocessing.outline.params, this.drawPass.depthTargetTransparent.texture, this.drawPass.depthTextureOpaque);
this.drawPass.postprocessing.outline.render();
@@ -348,7 +353,7 @@ export class IlluminationPass {
ValueCell.update(this.composeRenderable.values.dOcclusionEnable, occlusionEnabled);
}
if (props.postprocessing.occlusion.name === 'on') {
if (occlusionEnabled && props.postprocessing.occlusion.name === 'on') {
ValueCell.update(this.composeRenderable.values.uOcclusionColor, Color.toVec3Normalized(this.composeRenderable.values.uOcclusionColor.ref.value, props.postprocessing.occlusion.params.color));
}
@@ -370,7 +375,7 @@ export class IlluminationPass {
// background
const _toDrawingBuffer = toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing) && props.postprocessing.dof.name === 'off';
const _toDrawingBuffer = toDrawingBuffer && !antialiasingEnabled && !dofEnabled;
if (_toDrawingBuffer) {
this.webgl.bindDrawingBuffer();
} else {
@@ -384,7 +389,7 @@ export class IlluminationPass {
// compose
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing.background));
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing));
if (this.composeRenderable.values.dDenoise.ref.value !== props.illumination.denoise) {
ValueCell.update(this.composeRenderable.values.dDenoise, props.illumination.denoise);
needsUpdateCompose = true;
@@ -421,8 +426,8 @@ export class IlluminationPass {
let targetIsDrawingbuffer = false;
let swapTarget = this.outputTarget;
if (AntialiasingPass.isEnabled(props.postprocessing)) {
const _toDrawingBuffer = toDrawingBuffer && props.postprocessing.dof.name === 'off';
if (antialiasingEnabled) {
const _toDrawingBuffer = toDrawingBuffer && !dofEnabled;
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, _toDrawingBuffer ? true : this.outputTarget, props.postprocessing);
if (_toDrawingBuffer) {
@@ -433,13 +438,13 @@ export class IlluminationPass {
}
}
if (props.postprocessing.bloom.name === 'on') {
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
if (bloomEnabled && props.postprocessing.bloom.name === 'on') {
const _toDrawingBuffer = (toDrawingBuffer && !dofEnabled) || targetIsDrawingbuffer;
this.drawPass.bloom.update(this.tracing.colorTextureOpaque, this.tracing.normalTextureOpaque, this.drawPass.depthTextureOpaque, props.postprocessing.bloom.params);
this.drawPass.bloom.render(camera.viewport, _toDrawingBuffer ? undefined : this._colorTarget);
}
if (props.postprocessing.dof.name === 'on') {
if (dofEnabled && props.postprocessing.dof.name === 'on') {
const _toDrawingBuffer = toDrawingBuffer || targetIsDrawingbuffer;
this.drawPass.dof.update(camera, this._colorTarget.texture, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
this.drawPass.dof.render(camera.viewport, _toDrawingBuffer ? undefined : swapTarget);

View File

@@ -36,7 +36,7 @@ export type OutlineProps = PD.Values<typeof OutlineParams>
export class OutlinePass {
static isEnabled(props: PostprocessingProps) {
return props.outline.name !== 'off';
return props.enabled && props.outline.name !== 'off';
}
readonly target: RenderTarget;

View File

@@ -20,7 +20,7 @@ export class Passes {
constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, transparency: 'wboit' | 'dpoit' | 'blended' }> = {}) {
const drs = this.webgl.getDrawingBufferSize();
this.draw = new DrawPass(webgl, assetManager, drs.width, drs.height, attribs.transparency || 'blended');
this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
this.pick = new PickPass(webgl, drs.width, drs.height, attribs.pickScale || 0.25);
this.multiSample = new MultiSamplePass(webgl, this.draw);
this.illumination = new IlluminationPass(webgl, this.draw);
}
@@ -39,7 +39,7 @@ export class Passes {
const width = Math.max(drs.width, 2);
const height = Math.max(drs.height, 2);
this.draw.setSize(width, height);
this.pick.syncSize();
this.pick.setSize(width, height);
this.multiSample.syncSize();
this.illumination.setSize(width, height);
}

View File

@@ -7,6 +7,7 @@
import { PickingId } from '../../mol-geo/geometry/picking';
import { PickType, Renderer } from '../../mol-gl/renderer';
import { Scene } from '../../mol-gl/scene';
import { PixelPackBuffer } from '../../mol-gl/webgl/buffer';
import { isWebGL2 } from '../../mol-gl/webgl/compat';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
@@ -14,20 +15,29 @@ import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { Renderbuffer } from '../../mol-gl/webgl/renderbuffer';
import { Texture } from '../../mol-gl/webgl/texture';
import { Vec3 } from '../../mol-math/linear-algebra';
import { spiral2d } from '../../mol-math/misc';
import { isTimingMode } from '../../mol-util/debug';
import { unpackRGBToInt, unpackRGBAToDepth } from '../../mol-util/number-packing';
import { Camera, ICamera } from '../camera';
import { StereoCamera } from '../camera/stereo';
import { cameraUnproject } from '../camera/util';
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
import { now } from '../../mol-util/now';
import { unpackRGBAToDepth, unpackRGBToInt } from '../../mol-util/number-packing';
import { ICamera } from '../camera';
import { Viewport } from '../camera/util';
import { Helper } from '../helper/helper';
import { DrawPass } from './draw';
const NullId = Math.pow(2, 24) - 2;
export type PickData = { id: PickingId, position: Vec3 }
export type AsyncPickData = {
tryGet: () => 'pending' | PickData | undefined,
}
export const DefaultPickOptions = {
pickPadding: 1,
maxAsyncReadLag: 5,
};
export type PickOptions = typeof DefaultPickOptions
//
export class PickPass {
private readonly objectPickTarget: RenderTarget;
private readonly instancePickTarget: RenderTarget;
@@ -51,10 +61,10 @@ export class PickPass {
private pickWidth: number;
private pickHeight: number;
constructor(private webgl: WebGLContext, private drawPass: DrawPass, private pickScale: number) {
constructor(private webgl: WebGLContext, private width: number, private height: number, private pickScale: number) {
const pickRatio = pickScale / webgl.pixelRatio;
this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickRatio);
this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * pickRatio);
this.pickWidth = Math.ceil(width * pickRatio);
this.pickHeight = Math.ceil(height * pickRatio);
const { resources, extensions: { drawBuffers }, gl } = webgl;
@@ -109,13 +119,36 @@ export class PickPass {
}
}
dispose() {
if (this.webgl.extensions.drawBuffers) {
this.framebuffer.destroy();
this.objectPickTexture.destroy();
this.instancePickTexture.destroy();
this.groupPickTexture.destroy();
this.depthPickTexture.destroy();
this.objectPickFramebuffer.destroy();
this.instancePickFramebuffer.destroy();
this.groupPickFramebuffer.destroy();
this.depthPickFramebuffer.destroy();
this.depthRenderbuffer.destroy();
} else {
this.objectPickTarget.destroy();
this.instancePickTarget.destroy();
this.groupPickTarget.destroy();
this.depthPickTarget.destroy();
}
}
get pickRatio() {
return this.pickScale / this.webgl.pixelRatio;
}
setPickScale(pickScale: number) {
this.pickScale = pickScale;
this.syncSize();
this.setSize(this.width, this.height);
}
bindObject() {
@@ -151,13 +184,16 @@ export class PickPass {
}
get drawingBufferHeight() {
return this.drawPass.colorTarget.getHeight();
return this.height;
}
syncSize() {
setSize(width: number, height: number) {
this.width = width;
this.height = height;
const pickRatio = this.pickScale / this.webgl.pixelRatio;
const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickRatio);
const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * pickRatio);
const pickWidth = Math.ceil(this.width * pickRatio);
const pickHeight = Math.ceil(this.height * pickRatio);
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
this.pickWidth = pickWidth;
@@ -225,6 +261,7 @@ export class PickPass {
if (this.webgl.extensions.drawBuffers) {
this.framebuffer.bind();
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.None);
// printTextureImage(readTexture(this.webgl, this.groupPickTexture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true });
} else {
this.objectPickTarget.bind();
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Object);
@@ -234,7 +271,7 @@ export class PickPass {
this.groupPickTarget.bind();
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Group);
// printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' })
// printTextureImage(readTexture(this.webgl, this.groupPickTarget.texture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true });
this.depthPickTarget.bind();
this.renderVariant(renderer, camera, scene, helper, 'depth', PickType.None);
@@ -242,200 +279,220 @@ export class PickPass {
}
}
export class PickHelper {
dirty = true;
let AsyncPickingWarningShown = false;
private objectBuffer: Uint8Array;
private instanceBuffer: Uint8Array;
private groupBuffer: Uint8Array;
private depthBuffer: Uint8Array;
export function checkAsyncPickingSupport(webgl: WebGLContext): boolean {
if (webgl.isWebGL2) return true;
private viewport = Viewport();
if (isDebugMode && !AsyncPickingWarningShown) {
console.log('WebGL2 required for async picking. Falling back to synchronous picking.');
AsyncPickingWarningShown = true;
}
return false;
}
private pickRatio: number;
private pickX: number;
private pickY: number;
private pickWidth: number;
private pickHeight: number;
private halfPickWidth: number;
export enum AsyncPickStatus { Pending, Resolved, Failed };
private spiral: [number, number][];
export class PickBuffers {
private object: Uint8Array;
private instance: Uint8Array;
private group: Uint8Array;
private depth: Uint8Array;
private setupBuffers() {
const bufferSize = this.pickWidth * this.pickHeight * 4;
if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
this.objectBuffer = new Uint8Array(bufferSize);
this.instanceBuffer = new Uint8Array(bufferSize);
this.groupBuffer = new Uint8Array(bufferSize);
this.depthBuffer = new Uint8Array(bufferSize);
private objectBuffer: PixelPackBuffer;
private instanceBuffer: PixelPackBuffer;
private groupBuffer: PixelPackBuffer;
private depthBuffer: PixelPackBuffer;
private viewport = Viewport.create(0, 0, 0, 0);
private setup() {
const size = this.viewport.width * this.viewport.height * 4;
if (!this.object || this.object.length !== size) {
this.object = new Uint8Array(size);
this.instance = new Uint8Array(size);
this.group = new Uint8Array(size);
this.depth = new Uint8Array(size);
}
}
setViewport(x: number, y: number, width: number, height: number) {
Viewport.set(this.viewport, x, y, width, height);
this.update();
this.setup();
}
setPickPadding(pickPadding: number) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
private update() {
read() {
if (isTimingMode) this.webgl.timer.mark('PickBuffers.read');
const { x, y, width, height } = this.viewport;
this.pickRatio = this.pickPass.pickRatio;
this.pickX = Math.ceil(x * this.pickRatio);
this.pickY = Math.ceil(y * this.pickRatio);
const pickWidth = Math.floor(width * this.pickRatio);
const pickHeight = Math.floor(height * this.pickRatio);
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
this.pickWidth = pickWidth;
this.pickHeight = pickHeight;
this.halfPickWidth = Math.floor(this.pickWidth / 2);
this.setupBuffers();
}
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
this.dirty = true;
}
private syncBuffers() {
if (isTimingMode) this.webgl.timer.mark('PickHelper.syncBuffers');
const { pickX, pickY, pickWidth, pickHeight } = this;
this.pickPass.bindObject();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer);
this.webgl.readPixels(x, y, width, height, this.object);
this.pickPass.bindInstance();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer);
this.webgl.readPixels(x, y, width, height, this.instance);
this.pickPass.bindGroup();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer);
this.webgl.readPixels(x, y, width, height, this.group);
this.pickPass.bindDepth();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.syncBuffers');
this.webgl.readPixels(x, y, width, height, this.depth);
this.ready = true;
if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.read');
}
private getBufferIdx(x: number, y: number): number {
return (y * this.pickWidth + x) * 4;
private fenceSync: WebGLSync | null = null;
private fenceTimestamp: number = 0;
private ready = false;
private lag = 0;
asyncRead() {
const { gl } = this.webgl;
if (!isWebGL2(gl)) return;
if (isTimingMode) this.webgl.timer.mark('PickBuffers.asyncRead');
if (this.fenceSync !== null) {
gl.deleteSync(this.fenceSync);
}
const { x, y, width, height } = this.viewport;
this.pickPass.bindObject();
this.objectBuffer.read(x, y, width, height);
this.pickPass.bindInstance();
this.instanceBuffer.read(x, y, width, height);
this.pickPass.bindGroup();
this.groupBuffer.read(x, y, width, height);
this.pickPass.bindDepth();
this.depthBuffer.read(x, y, width, height);
this.fenceTimestamp = now();
this.fenceSync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
// gl.flush();
this.ready = false;
if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.asyncRead');
}
private getDepth(x: number, y: number): number {
const idx = this.getBufferIdx(x, y);
const b = this.depthBuffer;
check(): AsyncPickStatus {
if (this.ready) return AsyncPickStatus.Resolved;
if (this.fenceSync === null) return AsyncPickStatus.Failed;
const { gl } = this.webgl;
if (!isWebGL2(gl)) return AsyncPickStatus.Failed;
const res = gl.clientWaitSync(this.fenceSync, 0, 0);
if (res === gl.WAIT_FAILED || this.lag >= this.maxAsyncReadLag) {
// console.log(`failed to get buffer data after ${this.lag + 1} checks`);
if (res !== gl.WAIT_FAILED && now() - this.fenceTimestamp < 1000 / 60) {
this.lag += 1;
return AsyncPickStatus.Pending;
}
gl.deleteSync(this.fenceSync);
this.fenceSync = null;
this.lag = 0;
this.ready = false;
return AsyncPickStatus.Failed;
} else if (res === gl.TIMEOUT_EXPIRED) {
this.lag += 1;
// console.log(`waiting for buffer data for ${this.lag} checks`);
return AsyncPickStatus.Pending;
} else {
this.objectBuffer.getSubData(this.object);
this.instanceBuffer.getSubData(this.instance);
this.groupBuffer.getSubData(this.group);
this.depthBuffer.getSubData(this.depth);
// console.log(`got buffer data after ${this.lag + 1} checks`);
gl.deleteSync(this.fenceSync);
this.fenceSync = null;
this.lag = 0;
this.ready = true;
return AsyncPickStatus.Resolved;
}
}
private getIdx(x: number, y: number): number {
return (y * this.viewport.width + x) * 4;
}
getDepth(x: number, y: number): number {
if (!this.ready) return -1;
const idx = this.getIdx(x, y);
const b = this.depth;
return unpackRGBAToDepth(b[idx], b[idx + 1], b[idx + 2], b[idx + 3]);
}
private getId(x: number, y: number, buffer: Uint8Array) {
const idx = this.getBufferIdx(x, y);
if (!this.ready) return -1;
const idx = this.getIdx(x, y);
return unpackRGBToInt(buffer[idx], buffer[idx + 1], buffer[idx + 2]);
}
private render(camera: Camera | StereoCamera) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
renderer.setTransparentBackground(false);
renderer.setDrawingBufferSize(pickWidth, pickHeight);
renderer.setPixelRatio(this.pickRatio);
if (StereoCamera.is(camera)) {
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.left, scene, helper);
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.right, scene, helper);
} else {
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
this.pickPass.render(renderer, camera, scene, helper);
}
this.dirty = false;
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.render');
getObjectId(x: number, y: number) {
return this.getId(x, y, this.object);
}
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
if (this.pickRatio !== this.pickPass.pickRatio) {
this.update();
}
getInstanceId(x: number, y: number) {
return this.getId(x, y, this.instance);
}
const { webgl, pickRatio } = this;
if (webgl.isContextLost) return;
getGroupId(x: number, y: number) {
return this.getId(x, y, this.group);
}
x *= webgl.pixelRatio;
y *= webgl.pixelRatio;
y = this.pickPass.drawingBufferHeight - y; // flip y
const { viewport } = this;
// check if within viewport
if (x < viewport.x ||
y < viewport.y ||
x > viewport.x + viewport.width ||
y > viewport.y + viewport.height
) return;
if (this.dirty) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
this.render(camera);
this.syncBuffers();
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');
}
const xv = x - viewport.x;
const yv = y - viewport.y;
const xp = Math.floor(xv * pickRatio);
const yp = Math.floor(yv * pickRatio);
const objectId = this.getId(xp, yp, this.objectBuffer);
getPickingId(x: number, y: number): PickingId | undefined {
const objectId = this.getObjectId(x, y);
// console.log('objectId', objectId);
if (objectId === -1 || objectId === NullId) return;
const instanceId = this.getId(xp, yp, this.instanceBuffer);
const instanceId = this.getInstanceId(x, y);
// console.log('instanceId', instanceId);
if (instanceId === -1 || instanceId === NullId) return;
const groupId = this.getId(xp, yp, this.groupBuffer);
const groupId = this.getGroupId(x, y);
// console.log('groupId', groupId);
if (groupId === -1 || groupId === NullId) return;
const z = this.getDepth(xp, yp);
// console.log('z', z);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {
position[0] = viewport.x + (xv - halfWidth) * 2;
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
} else {
position[0] = viewport.x + xv * 2;
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
}
} else {
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
}
// console.log({ id: { objectId, instanceId, groupId }, position });
return { id: { objectId, instanceId, groupId }, position };
return { objectId, instanceId, groupId };
}
identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
for (const d of this.spiral) {
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
if (pickData) return pickData;
reset() {
this.fenceSync = null;
this.ready = false;
this.lag = 0;
this.fenceTimestamp = 0;
}
dispose() {
const { gl } = this.webgl;
if (!isWebGL2(gl)) return;
this.objectBuffer.destroy();
this.instanceBuffer.destroy();
this.groupBuffer.destroy();
this.depthBuffer.destroy();
if (this.fenceSync !== null) {
gl.deleteSync(this.fenceSync);
this.fenceSync = null;
}
}
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, private pickPadding = 1) {
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
constructor(private webgl: WebGLContext, private pickPass: PickPass, public maxAsyncReadLag = 5) {
if (webgl.isWebGL2) {
this.objectBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
this.instanceBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
this.groupBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
this.depthBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
}
this.setup();
}
}
}

View File

@@ -120,59 +120,60 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, t
}
export const PostprocessingParams = {
enabled: PD.Boolean(true),
occlusion: PD.MappedStatic('on', {
on: PD.Group(SsaoParams),
off: PD.Group({})
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect', hideIf: p => p.enabled === false }),
shadow: PD.MappedStatic('off', {
on: PD.Group(ShadowParams),
off: PD.Group({})
}, { cycle: true, description: 'Simplistic shadows' }),
}, { cycle: true, description: 'Simplistic shadows', hideIf: p => p.enabled === false }),
outline: PD.MappedStatic('off', {
on: PD.Group(OutlineParams),
off: PD.Group({})
}, { cycle: true, description: 'Draw outline around 3D objects' }),
}, { cycle: true, description: 'Draw outline around 3D objects', hideIf: p => p.enabled === false }),
dof: PD.MappedStatic('off', {
on: PD.Group(DofParams),
off: PD.Group({})
}, { cycle: true, description: 'DOF' }),
}, { cycle: true, description: 'DOF', hideIf: p => p.enabled === false }),
antialiasing: PD.MappedStatic('smaa', {
fxaa: PD.Group(FxaaParams),
smaa: PD.Group(SmaaParams),
off: PD.Group({})
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges', hideIf: p => p.enabled === false }),
sharpening: PD.MappedStatic('off', {
on: PD.Group(CasParams),
off: PD.Group({})
}, { cycle: true, description: 'Contrast Adaptive Sharpening' }),
background: PD.Group(BackgroundParams, { isFlat: true }),
}, { cycle: true, description: 'Contrast Adaptive Sharpening', hideIf: p => p.enabled === false }),
background: PD.Group(BackgroundParams, { isFlat: true, hideIf: p => p.enabled === false }),
bloom: PD.MappedStatic('on', {
on: PD.Group(BloomParams),
off: PD.Group({})
}, { cycle: true, description: 'Bloom' }),
}, { cycle: true, description: 'Bloom', hideIf: p => p.enabled === false }),
};
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
export class PostprocessingPass {
static isEnabled(props: PostprocessingProps) {
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
return props.enabled && (SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off');
}
static isTransparentDepthRequired(scene: Scene, props: PostprocessingProps) {
return DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props);
return props.enabled && (DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props));
}
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
return props.enabled && OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
}
static isTransparentSsaoEnabled(scene: Scene, props: PostprocessingProps) {
return SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
return props.enabled && SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
}
static isSsaoEnabled(props: PostprocessingProps) {
return SsaoPass.isEnabled(props);
return props.enabled && SsaoPass.isEnabled(props);
}
readonly target: RenderTarget;
@@ -354,7 +355,7 @@ export class PostprocessingPass {
export class AntialiasingPass {
static isEnabled(props: PostprocessingProps) {
return props.antialiasing.name !== 'off';
return props.enabled && (props.antialiasing.name !== 'off' || props.sharpening.name !== 'off');
}
readonly target: RenderTarget;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 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>
@@ -35,7 +35,7 @@ export type ShadowProps = PD.Values<typeof ShadowParams>
export class ShadowPass {
static isEnabled(props: PostprocessingProps) {
return props.shadow.name !== 'off';
return props.enabled && props.shadow.name !== 'off';
}
readonly target: RenderTarget;
@@ -83,8 +83,8 @@ export class ShadowPass {
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance);
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance);
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance * camera.state.scale);
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance * camera.state.scale);
if (this.renderable.values.dSteps.ref.value !== props.steps) {
ValueCell.update(this.renderable.values.dSteps, props.steps);
needsUpdateShadows = true;

View File

@@ -63,7 +63,7 @@ type Levels = {
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
function getLevels(props: { radius: number, bias: number }[], scale: number, levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
@@ -72,7 +72,7 @@ function getLevels(props: { radius: number, bias: number }[], levels?: Levels):
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);
radius[i] = Math.pow(2, p.radius) * scale;
bias[i] = p.bias;
}
return { count, radius, bias };
@@ -306,8 +306,8 @@ export class SsaoPass {
ValueCell.update(this.blurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.blurSecondPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias * camera.state.scale);
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias * camera.state.scale);
if (this.blurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
@@ -349,7 +349,7 @@ export class SsaoPass {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
const levels = getLevels(mp.levels, camera.state.scale);
ValueCell.updateIfChanged(this.renderable.values.dLevels, levels.count);
ValueCell.update(this.renderable.values.uLevelRadius, levels.radius);
@@ -358,7 +358,7 @@ export class SsaoPass {
ValueCell.updateIfChanged(this.renderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.renderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius));
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius) * camera.state.scale);
}
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -12,6 +12,7 @@ import { Cylinder, CylinderProps, DefaultCylinderProps } from '../../../primitiv
import { Prism } from '../../../primitive/prism';
import { polygon } from '../../../primitive/polygon';
import { hashFnv32a } from '../../../../mol-data/util';
import { Ray3D } from '../../../../mol-math/geometry/primitives/ray3d';
const cylinderMap = new Map<number, Primitive>();
const up = Vec3.create(0, 1, 0);
@@ -77,6 +78,11 @@ export function addSimpleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
}
export function addCylinderFromRay3D(state: MeshBuilder.State, ray: Ray3D, length: number, props: BasicCylinderProps) {
setCylinderMat(tmpCylinderMat, ray.origin, ray.direction, length, false);
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
}
export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: BasicCylinderProps) {
const d = Vec3.distance(start, end) * lengthScale;
Vec3.sub(tmpCylinderDir, end, start);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -22,10 +22,4 @@ export namespace Object3D {
up: Vec3.create(0, 1, 0),
};
}
const center = Vec3.zero();
export function update(object3d: Object3D) {
Vec3.add(center, object3d.position, object3d.direction);
Mat4.lookAt(object3d.view, object3d.position, center, object3d.up);
}
}

View File

@@ -130,11 +130,14 @@ export const GlobalUniformSchema = {
uInvProjection: UniformSpec('m4'),
uModelViewProjection: UniformSpec('m4'),
uInvModelViewProjection: UniformSpec('m4'),
uHasHeadRotation: UniformSpec('b'),
uInvHeadRotation: UniformSpec('m4'),
uIsOrtho: UniformSpec('f'),
uPixelRatio: UniformSpec('f'),
uViewport: UniformSpec('v4'),
uViewOffset: UniformSpec('v2'),
uModelScale: UniformSpec('f'),
uDrawingBufferSize: UniformSpec('v2'),
uCameraPosition: UniformSpec('v3'),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -50,6 +50,7 @@ const DefaultPrintImageOptions = {
id: 'molstar.debug.image',
normalize: false,
useCanvas: false,
flipY: false,
};
export type PrintImageOptions = typeof DefaultPrintImageOptions
@@ -101,6 +102,7 @@ export function printImageData(imageData: ImageData, options: Partial<PrintImage
tmpContainer.style.right = '0px';
tmpContainer.style.border = 'solid orange';
tmpContainer.style.pointerEvents = 'none';
if (options.flipY) tmpContainer.style.transform = 'scaleY(-1)';
document.body.appendChild(tmpContainer);
}

View File

@@ -152,6 +152,16 @@ function getLight(props: RendererProps['light'], light?: Light): Light {
return { count, direction, color };
}
export function getTransformedLightDirection(light: Light, t: Mat4): Light['direction'] {
const tld = new Array(light.count * 3);
for (let i = 0, il = light.count; i < il; ++i) {
Vec3.fromArray(tmpDir, light.direction, i * 3);
Vec3.transformDirection(tmpDir, tmpDir, t);
Vec3.toArray(tmpDir, tld, i * 3);
}
return tld;
}
namespace Renderer {
const enum Flag {
None = 0,
@@ -184,6 +194,7 @@ namespace Renderer {
['tDepth', emptyDepthTexture]
];
const model = Mat4();
const view = Mat4();
const invView = Mat4();
const modelView = Mat4();
@@ -191,6 +202,7 @@ namespace Renderer {
const invProjection = Mat4();
const modelViewProjection = Mat4();
const invModelViewProjection = Mat4();
const invHeadRotation = Mat4();
const cameraDir = Vec3();
const cameraPosition = Vec3();
@@ -198,6 +210,9 @@ namespace Renderer {
const viewOffset = Vec2();
const frustum = Frustum3D();
let modelScale = 1;
const boundingSphere = Sphere3D();
const ambientColor = Vec3();
Vec3.scale(ambientColor, Color.toArrayNormalized(p.ambientColor, ambientColor, 0), p.ambientIntensity);
@@ -213,9 +228,12 @@ namespace Renderer {
uProjection: ValueCell.create(Mat4()),
uModelViewProjection: ValueCell.create(modelViewProjection),
uInvModelViewProjection: ValueCell.create(invModelViewProjection),
uHasHeadRotation: ValueCell.create(false),
uInvHeadRotation: ValueCell.create(invHeadRotation),
uIsOrtho: ValueCell.create(1),
uViewOffset: ValueCell.create(viewOffset),
uModelScale: ValueCell.create(1),
uPixelRatio: ValueCell.create(ctx.pixelRatio),
uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
@@ -274,28 +292,33 @@ namespace Renderer {
return;
}
if (!Frustum3D.intersectsSphere3D(frustum, r.values.boundingSphere.ref.value)) {
Sphere3D.scaleNX(boundingSphere, r.values.boundingSphere.ref.value, modelScale);
if (!Frustum3D.intersectsSphere3D(frustum, boundingSphere)) {
return;
}
const [minDistance, maxDistance] = r.values.uLod.ref.value;
if (minDistance !== 0 || maxDistance !== 0) {
const { center, radius } = r.values.boundingSphere.ref.value;
const { center, radius } = boundingSphere;
const d = Plane3D.distanceToPoint(cameraPlane, center);
if (d + radius < minDistance) return;
if (d - radius > maxDistance) return;
if (d + radius < minDistance * modelScale) return;
if (d - radius > maxDistance * modelScale) return;
}
if (isOccluded !== null && isOccluded(r.values.boundingSphere.ref.value)) {
return;
}
const unscaled = modelScale === 1;
if (unscaled) {
if (isOccluded !== null && isOccluded(boundingSphere)) {
return;
}
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 0;
const hasMultipleInstances = r.values.uInstanceCount.ref.value > 1;
if (hasInstanceGrid && (hasMultipleInstances || r.values.lodLevels)) {
r.cull(cameraPlane, frustum, isOccluded, ctx.stats);
} else {
r.uncull();
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 0;
const hasMultipleInstances = r.values.uInstanceCount.ref.value > 1;
if (hasInstanceGrid && (hasMultipleInstances || r.values.lodLevels)) {
r.cull(cameraPlane, frustum, isOccluded, ctx.stats);
} else {
r.uncull();
}
}
let needUpdate = false;
@@ -382,9 +405,12 @@ namespace Renderer {
ValueCell.updateIfChanged(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0);
ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0));
ValueCell.updateIfChanged(globalUniforms.uModelScale, camera.state.scale);
ValueCell.update(globalUniforms.uCameraPosition, Vec3.copy(cameraPosition, camera.state.position));
ValueCell.update(globalUniforms.uCameraDir, Vec3.normalize(cameraDir, Vec3.sub(cameraDir, camera.state.target, camera.state.position)));
ValueCell.update(globalUniforms.uCameraPosition, Mat4.getTranslation(cameraPosition, invView));
const cameraTarget = Vec3.scale(Vec3(), camera.state.target, camera.state.scale);
Vec3.normalize(cameraDir, Vec3.sub(cameraDir, cameraTarget, cameraPosition));
ValueCell.update(globalUniforms.uCameraDir, cameraDir);
ValueCell.updateIfChanged(globalUniforms.uFar, camera.far);
ValueCell.updateIfChanged(globalUniforms.uNear, camera.near);
@@ -400,13 +426,26 @@ namespace Renderer {
ValueCell.update(globalUniforms.uCameraPlane, Plane3D.toArray(cameraPlane, globalUniforms.uCameraPlane.ref.value, 0));
ValueCell.updateIfChanged(globalUniforms.uMarkerAverage, scene.markerAverage);
const hasHeadRotation = !Mat4.isZero(camera.headRotation);
if (hasHeadRotation) {
ValueCell.updateIfChanged(globalUniforms.uHasHeadRotation, hasHeadRotation);
ValueCell.update(globalUniforms.uInvHeadRotation, Mat4.invert(invHeadRotation, camera.headRotation));
ValueCell.update(globalUniforms.uLightDirection, getTransformedLightDirection(light, invHeadRotation));
} else {
ValueCell.update(globalUniforms.uHasHeadRotation, false);
ValueCell.update(globalUniforms.uInvHeadRotation, Mat4.id);
ValueCell.update(globalUniforms.uLightDirection, light.direction);
}
};
const updateInternal = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, renderMask: Mask, markingDepthTest: boolean) => {
arrayMapUpsert(sharedTexturesList, 'tDepth', depthTexture || emptyDepthTexture);
ValueCell.update(globalUniforms.uModel, group.view);
ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, camera.view, group.view));
modelScale = camera.state.scale;
ValueCell.update(globalUniforms.uModel, Mat4.scaleUniformly(model, group.view, camera.state.scale));
ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, camera.view, model));
ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView));
ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection));
ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));

View File

@@ -312,7 +312,6 @@ namespace Scene {
}
},
update(objects, keepBoundingSphere) {
Object3D.update(object3d);
if (objects) {
for (let i = 0, il = objects.length; i < il; ++i) {
renderableMap.get(objects[i])?.update();

View File

@@ -68,7 +68,6 @@ import { common } from './shader/chunks/common.glsl';
import { fade_lod } from './shader/chunks/fade-lod.glsl';
import { float_to_rgba } from './shader/chunks/float-to-rgba.glsl';
import { light_frag_params } from './shader/chunks/light-frag-params.glsl';
import { matrix_scale } from './shader/chunks/matrix-scale.glsl';
import { normal_frag_params } from './shader/chunks/normal-frag-params.glsl';
import { read_from_texture } from './shader/chunks/read-from-texture.glsl';
import { rgba_to_float } from './shader/chunks/rgba-to-float.glsl';
@@ -104,7 +103,6 @@ const ShaderChunks: { [k: string]: string } = {
fade_lod,
float_to_rgba,
light_frag_params,
matrix_scale,
normal_frag_params,
read_from_texture,
rgba_to_float,

View File

@@ -36,7 +36,7 @@ export const assign_color_varying = `
vec3 cgridPos = (uColorGridTransform.w * (position - uColorGridTransform.xyz)) / uColorGridDim;
vColor.rgb = texture3dFrom2dLinear(tColorGrid, cgridPos, uColorGridDim, uColorTexDim).rgb;
#elif defined(dColorType_volumeInstance)
vec3 cgridPos = (uColorGridTransform.w * (vModelPosition - uColorGridTransform.xyz)) / uColorGridDim;
vec3 cgridPos = (uColorGridTransform.w * (vModelPosition / uModelScale - uColorGridTransform.xyz)) / uColorGridDim;
vColor.rgb = texture3dFrom2dLinear(tColorGrid, cgridPos, uColorGridDim, uColorTexDim).rgb;
#endif
@@ -52,7 +52,7 @@ export const assign_color_varying = `
#elif defined(dOverpaintType_vertexInstance)
vOverpaint = readFromTexture(tOverpaint, int(aInstance) * uVertexCount + vertexId, uOverpaintTexDim);
#elif defined(dOverpaintType_volumeInstance)
vec3 ogridPos = (uOverpaintGridTransform.w * (vModelPosition - uOverpaintGridTransform.xyz)) / uOverpaintGridDim;
vec3 ogridPos = (uOverpaintGridTransform.w * (vModelPosition / uModelScale - uOverpaintGridTransform.xyz)) / uOverpaintGridDim;
vOverpaint = texture3dFrom2dLinear(tOverpaintGrid, ogridPos, uOverpaintGridDim, uOverpaintTexDim);
#endif
@@ -73,7 +73,7 @@ export const assign_color_varying = `
#elif defined(dEmissiveType_vertexInstance)
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
#elif defined(dEmissiveType_volumeInstance)
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition / uModelScale - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
#endif
vEmissive *= uEmissiveStrength;
@@ -87,7 +87,7 @@ export const assign_color_varying = `
#elif defined(dSubstanceType_vertexInstance)
vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + vertexId, uSubstanceTexDim);
#elif defined(dSubstanceType_volumeInstance)
vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition / uModelScale - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
vSubstance = texture3dFrom2dLinear(tSubstanceGrid, sgridPos, uSubstanceGridDim, uSubstanceTexDim);
#endif
@@ -104,7 +104,7 @@ export const assign_color_varying = `
#elif defined(dEmissiveType_vertexInstance)
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
#elif defined(dEmissiveType_volumeInstance)
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition / uModelScale - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
#endif
vEmissive *= uEmissiveStrength;
@@ -133,7 +133,7 @@ export const assign_color_varying = `
#elif defined(dTransparencyType_vertexInstance)
vTransparency = readFromTexture(tTransparency, int(aInstance) * uVertexCount + vertexId, uTransparencyTexDim).a;
#elif defined(dTransparencyType_volumeInstance)
vec3 tgridPos = (uTransparencyGridTransform.w * (vModelPosition - uTransparencyGridTransform.xyz)) / uTransparencyGridDim;
vec3 tgridPos = (uTransparencyGridTransform.w * (vModelPosition / uModelScale - uTransparencyGridTransform.xyz)) / uTransparencyGridDim;
vTransparency = texture3dFrom2dLinear(tTransparencyGrid, tgridPos, uTransparencyGridDim, uTransparencyTexDim).a;
#endif
vTransparency *= uTransparencyStrength;

View File

@@ -56,6 +56,7 @@ varying vec3 vModelPosition;
varying vec3 vViewPosition;
uniform vec2 uViewOffset;
uniform float uModelScale;
uniform float uNear;
uniform float uFar;

View File

@@ -46,6 +46,8 @@ uniform int uPickType;
varying vec3 vModelPosition;
varying vec3 vViewPosition;
uniform float uModelScale;
#if defined(noNonInstancedActiveAttribs)
// int() is needed for some Safari versions
// see https://bugs.webkit.org/show_bug.cgi?id=244152

View File

@@ -1,12 +0,0 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export const matrix_scale = `
float matrixScale(in mat4 m){
vec4 r = m[0];
return sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2]);
}
`;

View File

@@ -48,10 +48,10 @@ void main() {
mat4 modelTransform = uModel * aTransform;
vTransform = aTransform;
vTransform = modelTransform;
vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
vSize = size * aScale;
vSize = size * aScale * uModelScale;
vCap = aCap;
vModelPosition = (vStart + vEnd) * 0.5;

View File

@@ -29,11 +29,12 @@ precision highp int;
uniform mat4 uProjection, uTransform, uModelView, uModel, uView;
uniform vec3 uCameraDir;
uniform float uModelScale;
uniform sampler2D tDepth;
uniform vec2 uDrawingBufferSize;
varying vec3 vOrigPos;
varying vec3 vModelPosition;
varying float vInstance;
varying vec4 vBoundingSphere;
varying mat4 vTransform;
@@ -212,7 +213,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
vec3 distVec = startLoc - pos;
if (dot(distVec, distVec) > maxDistSq) break;
unitPos = v3m4(pos, cartnToUnit);
unitPos = v3m4(pos / uModelScale, cartnToUnit);
// continue when outside of grid
if (unitPos.x > posMax.x || unitPos.y > posMax.y || unitPos.z > posMax.z ||
@@ -228,7 +229,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
if (uJumpLength > 0.0 && value < 0.01) {
nextPos = pos + rayDir * uJumpLength;
nextValue = textureVal(v3m4(nextPos, cartnToUnit)).a;
nextValue = textureVal(v3m4(nextPos / uModelScale, cartnToUnit)).a;
if (nextValue < 0.01) {
prevValue = nextValue;
pos = nextPos;
@@ -361,15 +362,15 @@ void main() {
if (gl_FrontFacing)
discard;
vec3 rayDir = mix(normalize(vOrigPos - uCameraPosition), uCameraDir, uIsOrtho);
vec3 step = rayDir * uStepScale;
vec3 rayDir = mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
vec3 step = rayDir * uStepScale * uModelScale;
float boundingSphereNear = distance(vBoundingSphere.xyz, uCameraPosition) - vBoundingSphere.w;
float d = max(uNear, boundingSphereNear) - mix(0.0, distance(vOrigPos, uCameraPosition), uIsOrtho);
vec3 start = mix(uCameraPosition, vOrigPos, uIsOrtho) + (d * rayDir);
float d = max(uNear, boundingSphereNear) - mix(0.0, distance(vModelPosition, uCameraPosition), uIsOrtho);
vec3 start = mix(uCameraPosition, vModelPosition, uIsOrtho) + (d * rayDir);
gl_FragColor = raymarch(start, step, rayDir);
float fragmentDepth = calcDepth((uModelView * vec4(start, 1.0)).xyz);
float fragmentDepth = calcDepth((uView * vec4(start, 1.0)).xyz);
float preFogAlpha = clamp(preFogAlphaBlended, 0.0, 1.0);
#include wboit_write
#endif

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -12,11 +12,13 @@ attribute vec3 aPosition;
attribute mat4 aTransform;
attribute float aInstance;
uniform mat4 uModel;
uniform mat4 uModelView;
uniform mat4 uProjection;
uniform vec4 uInvariantBoundingSphere;
uniform float uModelScale;
varying vec3 vOrigPos;
varying vec3 vModelPosition;
varying float vInstance;
varying vec4 vBoundingSphere;
varying mat4 vTransform;
@@ -33,11 +35,11 @@ void main() {
vec4 unitCoord = vec4(aPosition + vec3(0.5), 1.0);
vec4 mvPosition = uModelView * aTransform * uUnitToCartn * unitCoord;
vOrigPos = (aTransform * uUnitToCartn * unitCoord).xyz;
vModelPosition = (uModel * aTransform * uUnitToCartn * unitCoord).xyz;
vInstance = aInstance;
vBoundingSphere = vec4(
(aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0)).xyz,
uInvariantBoundingSphere.w
(uModel * aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0)).xyz,
uModelScale * uInvariantBoundingSphere.w
);
vTransform = aTransform;

View File

@@ -92,7 +92,7 @@ void main(void){
vec3 vViewPosition = -vPointViewPosition;
fragmentDepth = gl_FragCoord.z;
#if !defined(dIgnoreLight) || defined(dXrayShaded) || defined(dRenderVariant_tracing)
pointDir.z -= cos(length(pointDir));
pointDir.z -= cos(length(pointDir)) * vRadius * 0.5;
cameraNormal = -normalize(pointDir);
#endif
interior = false;

View File

@@ -18,6 +18,8 @@ precision highp int;
uniform mat4 uModelView;
uniform mat4 uInvProjection;
uniform float uIsOrtho;
uniform bool uHasHeadRotation;
uniform mat4 uInvHeadRotation;
uniform vec2 uTexDim;
uniform sampler2D tPositionGroup;
@@ -29,8 +31,6 @@ varying float vRadius;
varying vec3 vPoint;
varying vec3 vPointViewPosition;
#include matrix_scale
/**
* Bounding rectangle of a clipped, perspective-projected 3D Sphere.
* Michael Mara, Morgan McGuire. 2013
@@ -81,7 +81,7 @@ void main(void){
#include assign_clipping_varying
#include assign_size
vRadius = size * matrixScale(uModelView);
vRadius = size * uModelScale;
vec4 position4 = vec4(position, 1.0);
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
@@ -107,6 +107,10 @@ void main(void){
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
mvCorner.xy += mapping * vRadius;
gl_Position = uProjection * mvCorner;
} else if (uHasHeadRotation) {
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
mvCorner.xy += mapping * vRadius * 1.4;
gl_Position = uProjection * mvCorner;
} else {
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
sphereProjection(mvPosition.xyz, vRadius, mapping);

View File

@@ -32,11 +32,11 @@ uniform float uOffsetZ;
uniform float uIsOrtho;
uniform float uPixelRatio;
uniform vec4 uViewport;
uniform mat4 uInvHeadRotation;
uniform bool uHasHeadRotation;
varying vec2 vTexCoord;
#include matrix_scale
void main(void){
int vertexId = VertexID;
@@ -48,7 +48,7 @@ void main(void){
vTexCoord = aTexCoord;
float scale = matrixScale(uModelView);
float scale = uModelScale;
float offsetX = uOffsetX * scale;
float offsetY = uOffsetY * scale;
@@ -75,9 +75,16 @@ void main(void){
offsetZ -= 0.001 * distance(uCameraPosition, (uProjection * mvCorner).xyz);
}
mvCorner.xy += aMapping * size * scale;
mvCorner.x += offsetX;
mvCorner.y += offsetY;
vec3 cornerOffset = vec3(0.0);
cornerOffset.xy += aMapping * size * scale;
cornerOffset.x += offsetX;
cornerOffset.y += offsetY;
if (uHasHeadRotation) {
mvCorner.xyz += (uInvHeadRotation * vec4(cornerOffset, 1.0)).xyz;
} else {
mvCorner.xyz += cornerOffset;
}
if (uIsOrtho == 1.0) {
mvCorner.z += offsetZ;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,12 +12,13 @@ import { assertUnreachable, ValueOf } from '../../mol-util/type-helpers';
import { GLRenderingContext, isWebGL2 } from './compat';
import { WebGLExtensions } from './extensions';
import { WebGLState } from './state';
import { getBytesPerElement, getFormat, getType, TextureFormat, TextureType } from './texture';
const getNextBufferId = idFactory();
export type UsageHint = 'static' | 'dynamic' | 'stream'
export type DataType = 'uint8' | 'int8' | 'uint16' | 'int16' | 'uint32' | 'int32' | 'float32'
export type BufferType = 'attribute' | 'elements' | 'uniform'
export type BufferType = 'attribute' | 'elements' | 'uniform' | 'pixel-pack'
export type DataTypeArrayType = {
'uint8': Uint8Array
@@ -36,6 +37,7 @@ export function getUsageHint(gl: GLRenderingContext, usageHint: UsageHint) {
case 'static': return gl.STATIC_DRAW;
case 'dynamic': return gl.DYNAMIC_DRAW;
case 'stream': return gl.STREAM_DRAW;
default: assertUnreachable(usageHint);
}
}
@@ -81,6 +83,12 @@ export function getBufferType(gl: GLRenderingContext, bufferType: BufferType) {
} else {
throw new Error('WebGL2 is required for uniform buffers');
}
case 'pixel-pack':
if (isWebGL2(gl)) {
return gl.PIXEL_PACK_BUFFER;
} else {
throw new Error('WebGL2 is required for pixel-pack buffers');
}
}
}
@@ -258,4 +266,63 @@ export function createElementsBuffer(gl: GLRenderingContext, array: ElementsType
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.getBuffer());
}
};
}
}
//
export interface PixelPackBuffer {
readonly id: number
readonly _type: number
readonly _format: number
readonly _bpe: number
read: (x: number, y: number, width: number, height: number) => void
getSubData: (array: ArrayType) => void
reset: () => void
destroy: () => void
}
export function createPixelPackBuffer(gl: WebGL2RenderingContext, extensions: WebGLExtensions, format: TextureFormat, type: TextureType): PixelPackBuffer {
let _buffer = getBuffer(gl);
const _type = getType(gl, extensions, type);
const _format = getFormat(gl, format, type);
const _bpe = getBytesPerElement(format, type);
function read(x: number, y: number, width: number, height: number) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, _buffer);
gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * _bpe, gl.STREAM_READ);
gl.readPixels(x, y, width, height, _format, _type, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}
function getSubData(array: ArrayType) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, _buffer);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, array);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}
let destroyed = false;
return {
id: getNextBufferId(),
_type,
_format,
_bpe,
read,
getSubData,
reset: () => {
_buffer = getBuffer(gl);
},
destroy: () => {
if (destroyed) return;
gl.deleteBuffer(_buffer);
destroyed = true;
}
};
}

View File

@@ -182,6 +182,7 @@ function createStats() {
resourceCounts: {
attribute: 0,
elements: 0,
pixelPack: 0,
framebuffer: 0,
program: 0,
renderbuffer: 0,
@@ -253,7 +254,6 @@ export interface WebGLContext {
bindDrawingBuffer: () => void
getDrawingBufferSize: () => { width: number, height: number }
readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => void
readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
waitForGpuCommandsComplete: () => Promise<void>
waitForGpuCommandsCompleteSync: () => void
getFenceSync: () => WebGLSync | null
@@ -304,43 +304,6 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
let pixelScale = props.pixelScale || 1;
let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>;
if (isWebGL2(gl)) {
const pbo = gl.createBuffer();
let _buffer: Uint8Array | undefined = void 0;
let _resolve: (() => void) | undefined = void 0;
let _reading = false;
const bindPBO = () => {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, _buffer!);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
_reading = false;
_resolve!();
_resolve = void 0;
_buffer = void 0;
};
readPixelsAsync = (x: number, y: number, width: number, height: number, buffer: Uint8Array): Promise<void> => new Promise<void>((resolve, reject) => {
if (_reading) {
reject('Can not call multiple readPixelsAsync at the same time');
return;
}
_reading = true;
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ);
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
// need to unbind/bind PBO before/after async awaiting the fence
_resolve = resolve;
_buffer = buffer;
fence(gl, bindPBO);
});
} else {
readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
readPixels(gl, x, y, width, height, buffer);
};
}
const renderTargets = new Set<RenderTarget>();
return {
@@ -429,7 +392,6 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => {
readPixels(gl, x, y, width, height, buffer);
},
readPixelsAsync,
waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
getFenceSync: () => {

View File

@@ -6,11 +6,11 @@
import { ProgramProps, createProgram, Program } from './program';
import { ShaderType, createShader, Shader, ShaderProps } from './shader';
import { GLRenderingContext } from './compat';
import { GLRenderingContext, isWebGL2 } from './compat';
import { Framebuffer, createFramebuffer } from './framebuffer';
import { WebGLExtensions } from './extensions';
import { WebGLState } from './state';
import { AttributeBuffer, UsageHint, ArrayType, AttributeItemSize, createAttributeBuffer, ElementsBuffer, createElementsBuffer, ElementsType, AttributeBuffers } from './buffer';
import { AttributeBuffer, UsageHint, ArrayType, AttributeItemSize, createAttributeBuffer, ElementsBuffer, createElementsBuffer, ElementsType, AttributeBuffers, PixelPackBuffer, createPixelPackBuffer } from './buffer';
import { createReferenceCache, ReferenceItem } from '../../mol-util/reference-cache';
import { WebGLStats } from './context';
import { hashString, hashFnv32a } from '../../mol-data/util';
@@ -54,6 +54,7 @@ type ByteCounts = {
export interface WebGLResources {
attribute: (array: ArrayType, itemSize: AttributeItemSize, divisor: number, usageHint?: UsageHint) => AttributeBuffer
elements: (array: ElementsType, usageHint?: UsageHint) => ElementsBuffer
pixelPack: (format: TextureFormat, type: TextureType) => PixelPackBuffer
framebuffer: () => Framebuffer
program: (defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema) => Program
renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
@@ -72,6 +73,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
const sets: { [k in ResourceName]: Set<Resource> } = {
attribute: new Set<Resource>(),
elements: new Set<Resource>(),
pixelPack: new Set<Resource>(),
framebuffer: new Set<Resource>(),
program: new Set<Resource>(),
renderbuffer: new Set<Resource>(),
@@ -126,6 +128,12 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
elements: (array: ElementsType, usageHint?: UsageHint) => {
return wrap('elements', createElementsBuffer(gl, array, usageHint));
},
pixelPack: (format: TextureFormat, type: TextureType) => {
if (!isWebGL2(gl)) {
throw new Error('WebGL2 is required for pixel-pack buffers');
}
return wrap('pixelPack', createPixelPackBuffer(gl, extensions, format, type));
},
framebuffer: () => {
return wrap('framebuffer', createFramebuffer(gl));
},
@@ -171,6 +179,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
reset: () => {
sets.attribute.forEach(r => r.reset());
sets.elements.forEach(r => r.reset());
sets.pixelPack.forEach(r => r.reset());
sets.framebuffer.forEach(r => r.reset());
sets.renderbuffer.forEach(r => r.reset());
sets.shader.forEach(r => r.reset());
@@ -182,6 +191,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
destroy: () => {
sets.attribute.forEach(r => r.destroy());
sets.elements.forEach(r => r.destroy());
sets.pixelPack.forEach(r => r.destroy());
sets.framebuffer.forEach(r => r.destroy());
sets.renderbuffer.forEach(r => r.destroy());
sets.shader.forEach(r => r.destroy());

View File

@@ -118,8 +118,11 @@ export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat,
}
function getByteCount(format: TextureFormat, type: TextureType, width: number, height: number, depth: number): number {
const bpe = getFormatSize(format) * getTypeSize(type);
return bpe * width * height * (depth || 1);
return getBytesPerElement(format, type) * width * height * (depth || 1);
}
export function getBytesPerElement(format: TextureFormat, type: TextureType): number {
return getFormatSize(format) * getTypeSize(type);
}
function getFormatSize(format: TextureFormat) {

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { Vec3 } from '../../linear-algebra';
import { Box3D } from '../primitives/box3d';
import { Ray3D } from '../primitives/ray3d';
describe('ray3d', () => {
it('intersectBox3D', () => {
const box = Box3D.create(Vec3.create(-1, -1, -1), Vec3.create(1, 1, 1));
const out = Vec3();
// 1. Ray starts outside and hits the box frontally
const ray1 = Ray3D.create(Vec3.create(-2, 0, 0), Vec3.create(1, 0, 0));
expect(Ray3D.intersectBox3D(out, ray1, box)).toBe(true);
expect(out).toEqual(Vec3.create(-1, 0, 0));
// 2. Ray grazes along the top edge (tangential)
const ray2 = Ray3D.create(Vec3.create(-2, 1, 0), Vec3.create(1, 0, 0));
expect(Ray3D.intersectBox3D(out, ray2, box)).toBe(true);
expect(out).toEqual(Vec3.create(-1, 1, 0));
// 3. Ray starts exactly on the surface and goes inward
const ray3 = Ray3D.create(Vec3.create(-1, 0, 0), Vec3.create(1, 0, 0));
expect(Ray3D.intersectBox3D(out, ray3, box)).toBe(true);
expect(out).toEqual(Vec3.create(-1, 0, 0));
// 4. Ray grazes a corner exactly
const ray4 = Ray3D.create(Vec3.create(-2, -2, -2), Vec3.create(1, 1, 1));
expect(Ray3D.intersectBox3D(out, ray4, box)).toBe(true);
expect(out).toEqual(Vec3.create(-1, -1, -1));
// 5. Ray starts inside the box and exits
const ray5 = Ray3D.create(Vec3.create(0, 0, 0), Vec3.create(1, 0, 0));
expect(Ray3D.intersectBox3D(out, ray5, box)).toBe(false);
// 6. Ray starts outside and points away (misses box completely)
const ray6 = Ray3D.create(Vec3.create(-2, 2, 0), Vec3.create(1, 0, 0));
expect(Ray3D.intersectBox3D(out, ray6, box)).toBe(false);
});
});

View File

@@ -418,7 +418,7 @@ function queryNearest<T extends number = number>(ctx: QueryContext, result: Resu
if (!Box3D.containsVec3(box, tmpRay.origin)) {
// intersect ray pointing to box center
Ray3D.targetTo(tmpRay, tmpRay, center);
Box3D.nearestIntersectionWithRay3D(tmpRay.origin, box, tmpRay);
Ray3D.intersectBox3D(tmpRay.origin, tmpRay, box);
gX = Math.max(0, Math.min(sX - 1, Math.floor((tmpRay.origin[0] - min[0]) / delta[0])));
gY = Math.max(0, Math.min(sY - 1, Math.floor((tmpRay.origin[1] - min[1]) / delta[1])));
gZ = Math.max(0, Math.min(sZ - 1, Math.floor((tmpRay.origin[2] - min[2]) / delta[2])));

View File

@@ -10,7 +10,6 @@ import { OrderedSet } from '../../../mol-data/int';
import { Sphere3D } from './sphere3d';
import { Vec3 } from '../../linear-algebra/3d/vec3';
import { Mat4 } from '../../linear-algebra/3d/mat4';
import { Ray3D } from './ray3d';
interface Box3D { min: Vec3, max: Vec3 }
@@ -191,48 +190,6 @@ namespace Box3D {
) ? false : true;
}
export function nearestIntersectionWithRay3D(out: Vec3, box: Box3D, ray: Ray3D): Vec3 {
const { origin, direction } = ray;
const [minX, minY, minZ] = box.min;
const [maxX, maxY, maxZ] = box.max;
const [x, y, z] = origin;
const invDirX = 1.0 / direction[0];
const invDirY = 1.0 / direction[1];
const invDirZ = 1.0 / direction[2];
let tmin, tmax, tymin, tymax, tzmin, tzmax;
if (invDirX >= 0) {
tmin = (minX - x) * invDirX;
tmax = (maxX - x) * invDirX;
} else {
tmin = (maxX - x) * invDirX;
tmax = (minX - x) * invDirX;
}
if (invDirY >= 0) {
tymin = (minY - y) * invDirY;
tymax = (maxY - y) * invDirY;
} else {
tymin = (maxY - y) * invDirY;
tymax = (minY - y) * invDirY;
}
if (invDirZ >= 0) {
tzmin = (minZ - z) * invDirZ;
tzmax = (maxZ - z) * invDirZ;
} else {
tzmin = (maxZ - z) * invDirZ;
tzmax = (minZ - z) * invDirZ;
}
if (tymin > tmin)
tmin = tymin;
if (tymax < tmax)
tmax = tymax;
if (tzmin > tmin)
tmin = tzmin;
if (tzmax < tmax)
tmax = tzmax;
Vec3.scale(out, direction, tmin);
return Vec3.set(out, out[0] + x, out[1] + y, out[2] + z);
}
export function center(out: Vec3, box: Box3D): Vec3 {
return Vec3.center(out, box.max, box.min);
}

View File

@@ -2,10 +2,13 @@
* Copyright (c) 2025 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 { Mat4 } from '../../linear-algebra/3d/mat4';
import { Vec3 } from '../../linear-algebra/3d/vec3';
import { Box3D } from './box3d';
import { Sphere3D } from './sphere3d';
interface Ray3D { origin: Vec3, direction: Vec3 }
@@ -38,6 +41,96 @@ namespace Ray3D {
Vec3.transformDirection(out.direction, ray.direction, m);
return out;
}
//
const tmpIR = Vec3();
function _intersectSphere3D(ray: Ray3D, sphere: Sphere3D): number {
const { center, radius } = sphere;
const { origin, direction } = ray;
const oc = Vec3.sub(tmpIR, origin, center);
const a = Vec3.dot(direction, direction);
const b = 2.0 * Vec3.dot(oc, direction);
const c = Vec3.dot(oc, oc) - radius * radius;
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) return -1; // no intersection
const t = (-b - Math.sqrt(discriminant)) / (2.0 * a);
if (t < 0) return -1; // behind the ray
return t;
}
export function intersectSphere3D(out: Vec3, ray: Ray3D, sphere: Sphere3D): boolean {
const t = _intersectSphere3D(ray, sphere);
if (t < 0) return false;
Vec3.scaleAndAdd(out, ray.origin, ray.direction, t);
return true;
}
export function isIntersectingSphere3D(ray: Ray3D, sphere: Sphere3D): boolean {
return _intersectSphere3D(ray, sphere) >= 0;
}
export function isInsideSphere3D(ray: Ray3D, sphere: Sphere3D): boolean {
return Vec3.distance(ray.origin, sphere.center) < sphere.radius;
}
//
function _intersectBox3D(ray: Ray3D, box: Box3D): number {
const { origin, direction } = ray;
const [minX, minY, minZ] = box.min;
const [maxX, maxY, maxZ] = box.max;
const [x, y, z] = origin;
const invDirX = 1.0 / direction[0];
const invDirY = 1.0 / direction[1];
const invDirZ = 1.0 / direction[2];
let tmin, tmax, tymin, tymax, tzmin, tzmax;
if (invDirX >= 0) {
tmin = (minX - x) * invDirX;
tmax = (maxX - x) * invDirX;
} else {
tmin = (maxX - x) * invDirX;
tmax = (minX - x) * invDirX;
}
if (invDirY >= 0) {
tymin = (minY - y) * invDirY;
tymax = (maxY - y) * invDirY;
} else {
tymin = (maxY - y) * invDirY;
tymax = (minY - y) * invDirY;
}
if ((tmin > tymax) || (tymin > tmax)) return -1;
if (tymin > tmin) tmin = tymin;
if (tymax < tmax) tmax = tymax;
if (invDirZ >= 0) {
tzmin = (minZ - z) * invDirZ;
tzmax = (maxZ - z) * invDirZ;
} else {
tzmin = (maxZ - z) * invDirZ;
tzmax = (minZ - z) * invDirZ;
}
if ((tmin > tzmax) || (tzmin > tmax)) return -1;
if (tzmin > tmin) tmin = tzmin;
if (tzmax < tmax) tmax = tzmax;
return tmin >= 0 ? tmin : -1;
}
export function intersectBox3D(out: Vec3, ray: Ray3D, box: Box3D): boolean {
const t = _intersectBox3D(ray, box);
if (t < 0) return false;
Vec3.scaleAndAdd(out, ray.origin, ray.direction, t);
return true;
}
export function isIntersectingBox3D(ray: Ray3D, box: Box3D): boolean {
return _intersectBox3D(ray, box) >= 0;
}
}
export { Ray3D };

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
@@ -109,6 +109,23 @@ namespace Sphere3D {
return out;
}
/** Scale sphere by a number */
export function scale(out: Sphere3D, sphere: Sphere3D, s: number) {
Vec3.scale(out.center, sphere.center, s);
out.radius = sphere.radius * s;
if (hasExtrema(sphere)) {
setExtrema(out, sphere.extrema.map(e => Vec3.scale(Vec3(), e, s)));
}
return out;
}
/** Scale sphere by a number but without extrema */
export function scaleNX(out: Sphere3D, sphere: Sphere3D, s: number) {
Vec3.scale(out.center, sphere.center, s);
out.radius = sphere.radius * s;
return out;
}
export function toArray<T extends NumberArray>(s: Sphere3D, out: T, offset: number) {
Vec3.toArray(s.center, out, offset);
out[offset + 3] = s.radius;

View File

@@ -84,6 +84,11 @@ namespace Mat4 {
return mat;
}
export function isZero(mat: Mat4): boolean {
for (let i = 0; i < 16; i++) if (mat[i] !== 0) return false;
return true;
}
export function setZero(mat: Mat4): Mat4 {
for (let i = 0; i < 16; i++) mat[i] = 0;
return mat;

View File

@@ -42,7 +42,7 @@ export interface Shape<G extends Geometry = Geometry> {
}
export namespace Shape {
export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> {
export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[], groupCount?: number): Shape<G> {
return {
id: UUID.create22(),
name,
@@ -50,8 +50,7 @@ export namespace Shape {
geometry,
transforms: transforms || [Mat4.identity()],
get groupCount() {
// TODO: consider adding an way provide the group count explicitely
return Geometry.getGroupCount(geometry);
return groupCount ?? Geometry.getGroupCount(geometry);
},
getColor,
getSize,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Jesse Liang <jesse.liang@rcsb.org>
@@ -158,6 +158,7 @@ export function defaultCanvas3DParams(): Partial<Canvas3DProps> {
},
fov: 90,
manualReset: false,
scale: 1,
},
cameraResetDurationMs: 0,
cameraFog: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
@@ -9,10 +9,9 @@
import { Subject, Observable } from 'rxjs';
import { Viewport } from '../../mol-canvas3d/camera/util';
import { Vec2, EPSILON } from '../../mol-math/linear-algebra';
import { BitFlags, noop } from '../../mol-util';
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d';
export function getButtons(event: MouseEvent | Touch) {
if (typeof event === 'object') {
@@ -145,6 +144,7 @@ export type DragInput = {
pageX: number,
pageY: number,
isStart: boolean
useDelta?: boolean
} & BaseInput
export type WheelInput = {
@@ -164,6 +164,7 @@ export type ClickInput = {
y: number,
pageX: number,
pageY: number,
ray?: Ray3D,
} & BaseInput
export type MoveInput = {
@@ -171,6 +172,7 @@ export type MoveInput = {
y: number,
pageX: number,
pageY: number,
ray?: Ray3D,
movementX?: number,
movementY?: number,
inside: boolean,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -14,7 +14,7 @@ import { EveryLoci } from '../../mol-model/loci';
import { RuntimeContext, Progress } from '../../mol-task';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { Sphere } from '../../mol-geo/primitive/sphere';
import { ColorNames } from '../../mol-util/color/names';
import { Shape } from '../../mol-model/shape';
@@ -52,7 +52,7 @@ parent.appendChild(info);
let prevReprLoci = Representation.Loci.Empty;
canvas3d.input.move.subscribe(({ x, y }) => {
const pickingId = canvas3d.identify(x, y)?.id;
const pickingId = canvas3d.identify(Vec2.create(x, y))?.id;
let label = '';
if (pickingId) {
const reprLoci = canvas3d.getLoci(pickingId);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -28,6 +28,7 @@ import { SyncRuntimeContext } from '../../mol-task/execution/synchronous';
import { AssetManager } from '../../mol-util/assets';
import { MembraneOrientationProvider } from '../../extensions/anvil/prop';
import { MembraneOrientationRepresentationProvider } from '../../extensions/anvil/representation';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
const parent = document.getElementById('app')!;
parent.style.width = '100%';
@@ -56,7 +57,7 @@ parent.appendChild(info);
let prevReprLoci = Representation.Loci.Empty;
canvas3d.input.move.pipe(throttleTime(100)).subscribe(({ x, y }) => {
const pickingId = canvas3d.identify(x, y)?.id;
const pickingId = canvas3d.identify(Vec2.create(x, y))?.id;
let label = '';
if (pickingId) {
const reprLoci = canvas3d.getLoci(pickingId);