diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d903f6b4c84a9bea1b672afac438e40a99866eb..e1e8d5e99ba39aa97b6efcf682c82b8044c4dd97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ Note that since we don't clearly distinguish between a public and private interf - Add ``fov`` (Field of View) Canvas3D parameter - Add ``sceneRadiusFactor`` Canvas3D parameter - Add background pass (skybox, image, horizontal/radial gradient) + - Set simple-settings presets via ``PluginConfig.Background.Styles`` + - Example presets in new backgrounds extension + - Load skybox/image from URL or File (saved in session) + - Opacity, saturation, lightness controls for skybox/image + - Coverage (viewport or canvas) controls for image/gradient +- [Breaking] ``AssetManager`` needs to be passed to various graphics related classes ## [v3.13.0] - 2022-07-24 diff --git a/package.json b/package.json index 24339001b49150ed20300fee9c65ac54bdeffc1b..4ef0af12c3397cc1b2ba736997436aab1d76f45e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "rebuild": "npm run clean && npm run build", "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer", "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"", - "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/", + "build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/", "build-webpack": "webpack --mode production --config ./webpack.config.production.js", "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js", "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"", @@ -28,7 +28,7 @@ "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"", "watch-tsc": "tsc --watch --incremental", "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental", - "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch", + "watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch", "watch-webpack": "webpack -w --mode development --stats minimal", "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js", "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js", diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index 2ab5c5eafb53157ff9ca975cb4041cbf990f8167..8a27a9a4cd9762fc7686d621b145ddc0faed6bfa 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -46,6 +46,7 @@ import { Color } from '../../mol-util/color'; import '../../mol-util/polyfill'; import { ObjectKeys } from '../../mol-util/type-helpers'; import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants'; +import { Backgrounds } from '../../extensions/backgrounds'; export { PLUGIN_VERSION as version } from '../../mol-plugin/version'; export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug'; @@ -55,6 +56,7 @@ const CustomFormats = [ ]; const Extensions = { + 'backgrounds': PluginSpec.Behavior(Backgrounds), 'cellpack': PluginSpec.Behavior(CellPack), 'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids), 'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport), diff --git a/src/extensions/backgrounds/images/cells.jpg b/src/extensions/backgrounds/images/cells.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3502c798c5d012acd67640ca8cbbb2341014c71f Binary files /dev/null and b/src/extensions/backgrounds/images/cells.jpg differ diff --git a/src/extensions/backgrounds/index.ts b/src/extensions/backgrounds/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c87c8899f367a38bf377f6d266586faf0fe32ae0 --- /dev/null +++ b/src/extensions/backgrounds/index.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginBehavior } from '../../mol-plugin/behavior/behavior'; +import { PluginConfig } from '../../mol-plugin/config'; +import { Color } from '../../mol-util/color/color'; + +// from https://visualsonline.cancer.gov/details.cfm?imageid=2304, public domain +import image_cells from './images/cells.jpg'; + +// created with http://alexcpeterson.com/spacescape/ +import face_nebula_nx from './skyboxes/nebula/nebula_left2.jpg'; +import face_nebula_ny from './skyboxes/nebula/nebula_bottom4.jpg'; +import face_nebula_nz from './skyboxes/nebula/nebula_back6.jpg'; +import face_nebula_px from './skyboxes/nebula/nebula_right1.jpg'; +import face_nebula_py from './skyboxes/nebula/nebula_top3.jpg'; +import face_nebula_pz from './skyboxes/nebula/nebula_front5.jpg'; + +export const Backgrounds = PluginBehavior.create<{ }>({ + name: 'extension-backgrounds', + category: 'misc', + display: { + name: 'Backgrounds' + }, + ctor: class extends PluginBehavior.Handler<{ }> { + register(): void { + this.ctx.config.set(PluginConfig.Background.Styles, [ + [{ + variant: { + name: 'radialGradient', + params: { + centerColor: Color(0xFFFFFF), + edgeColor: Color(0x808080), + ratio: 0.2, + coverage: 'viewport', + } + } + }, 'Light Radial Gradient'], + [{ + variant: { + name: 'image', + params: { + source: { + name: 'url', + params: image_cells + }, + lightness: 0, + saturation: 0, + opacity: 1, + coverage: 'viewport', + } + } + }, 'Normal Cells Image'], + [{ + variant: { + name: 'skybox', + params: { + faces: { + name: 'urls', + params: { + nx: face_nebula_nx, + ny: face_nebula_ny, + nz: face_nebula_nz, + px: face_nebula_px, + py: face_nebula_py, + pz: face_nebula_pz, + } + }, + lightness: 0, + saturation: 0, + opacity: 1, + } + } + }, 'Purple Nebula Skybox'], + ]); + } + + update() { + return false; + } + + unregister() { + this.ctx.config.set(PluginConfig.Background.Styles, []); + } + }, + params: () => ({ }) +}); diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e2f0fd8d977272a4bc2af9cf982fe866eb52186 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2be6e805e2ea0103230cf51637218e115cf0a76d Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e9c0674db6f28f3d0fa02421e473517790d51f2d Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..810037b66dc4601b24b1b1b9adf514f626247b6c Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..059d46bf8e7d8d18ce12259679f0c53ad4a5dc27 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..831b81964a253fbbd08b0ba3aad74100ce55fad1 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg differ diff --git a/src/extensions/backgrounds/typings.d.ts b/src/extensions/backgrounds/typings.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..94ad23bc10cbec9f2049cdc72b60aca6b201043d --- /dev/null +++ b/src/extensions/backgrounds/typings.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +declare module '*.jpg' { + const value: any; + export = value; +} diff --git a/src/extensions/mp4-export/encoder.ts b/src/extensions/mp4-export/encoder.ts index 5dc12af421808abee78e69e341f7b4d7ae263e4f..38e96599220a377c94704939d9a951be46d1ff17 100644 --- a/src/extensions/mp4-export/encoder.ts +++ b/src/extensions/mp4-export/encoder.ts @@ -69,6 +69,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: const dt = durationMs / N; await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 }); + await params.pass.updateBackground(); await plugin.managers.animation.play(params.animation.definition, params.animation.params); stoppedAnimation = false; diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 22cb2d363e8611c076cb522d31f78e583e41e917..5df5536af201a1b921c1a5c84a9dba55642f9afe 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -40,8 +40,8 @@ import { Passes } from './passes/passes'; import { shallowEqual } from '../mol-util'; import { MarkingParams } from './passes/marking'; import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item'; -import { BackgroundPass } from './passes/background'; import { degToRad, radToDeg } from '../mol-math/misc'; +import { AssetManager } from '../mol-util/assets'; export const Canvas3DParams = { camera: PD.Group({ @@ -110,6 +110,7 @@ interface Canvas3DContext { readonly attribs: Readonly<Canvas3DContext.Attribs> readonly contextLost: BehaviorSubject<now.Timestamp> readonly contextRestored: BehaviorSubject<now.Timestamp> + readonly assetManager: AssetManager dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void } @@ -128,7 +129,7 @@ namespace Canvas3DContext { }; export type Attribs = typeof DefaultAttribs - export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext { + export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext { const a = { ...DefaultAttribs, ...attribs }; const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a; const gl = getGLContext(canvas, { @@ -143,7 +144,7 @@ namespace Canvas3DContext { const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true }); const webgl = createContext(gl, { pixelScale }); - const passes = new Passes(webgl, a); + const passes = new Passes(webgl, assetManager, a); if (isDebugMode) { const loseContextExt = gl.getExtension('WEBGL_lose_context'); @@ -196,6 +197,7 @@ namespace Canvas3DContext { attribs: a, contextLost, contextRestored: webgl.contextRestored, + assetManager, dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => { input.dispose(); @@ -282,9 +284,8 @@ namespace Canvas3D { export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 } export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 } - export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D { + export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D { const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props }; - BackgroundPass.loadTexture(webgl, p.postprocessing.background, () => requestDraw()); const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>(); const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>(); @@ -325,6 +326,10 @@ namespace Canvas3D { const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction); const multiSampleHelper = new MultiSampleHelper(passes.multiSample); + passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => { + if (changed) requestDraw(); + }); + let cameraResetRequested = false; let nextCameraResetDuration: number | undefined = void 0; let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0; @@ -827,15 +832,10 @@ namespace Canvas3D { } if (props.postprocessing?.background) { - const newBackground = { ...p.postprocessing.background, ...props.postprocessing.background }; - if (!BackgroundPass.areTexturePropsEqual(newBackground, p.postprocessing.background)) { - Object.assign(p.postprocessing.background, props.postprocessing.background); - BackgroundPass.loadTexture(webgl, p.postprocessing.background, () => { - if (!doNotRequestDraw) requestDraw(); - }); - } else { - Object.assign(p.postprocessing.background, props.postprocessing.background); - } + Object.assign(p.postprocessing.background, props.postprocessing.background); + passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => { + if (changed && !doNotRequestDraw) requestDraw(); + }); } if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing); if (props.marking) Object.assign(p.marking, props.marking); @@ -855,7 +855,7 @@ namespace Canvas3D { } }, getImagePass: (props: Partial<ImageProps> = {}) => { - return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props); + return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, props); }, getRenderObjects(): GraphicsRenderObject[] { const renderObjects: GraphicsRenderObject[] = []; diff --git a/src/mol-canvas3d/passes/background.ts b/src/mol-canvas3d/passes/background.ts index a80388aad43b677936252bd7c5fe4c651f9ad6d7..6f0b4832f61f956656ced8c99345e9cc7315fda0 100644 --- a/src/mol-canvas3d/passes/background.ts +++ b/src/mol-canvas3d/passes/background.ts @@ -12,7 +12,7 @@ import { background_frag } from '../../mol-gl/shader/background.frag'; import { background_vert } from '../../mol-gl/shader/background.vert'; import { WebGLContext } from '../../mol-gl/webgl/context'; import { createComputeRenderItem } from '../../mol-gl/webgl/render-item'; -import { createCubeTexture, createNullTexture, createTexture, CubeFaces, ImageTexture, Texture } from '../../mol-gl/webgl/texture'; +import { createNullTexture, CubeFaces, Texture } from '../../mol-gl/webgl/texture'; import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4'; import { ValueCell } from '../../mol-util/value-cell'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; @@ -21,9 +21,16 @@ import { Camera, ICamera } from '../camera'; import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3'; import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2'; import { Color } from '../../mol-util/color'; +import { Asset, AssetManager } from '../../mol-util/assets'; +import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4'; + +const SharedParams = { + opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }), + saturation: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }), + lightness: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }), +}; const SkyboxParams = { - size: PD.Select(512, [[256, '256x256'], [512, '512x512'], [1024, '1024x1024'], [2048, '2048x2048'], [4096, '4096x4096']] as const), // TODO: remove faces: PD.MappedStatic('urls', { urls: PD.Group({ nx: PD.Text('', { label: 'Negative X' }), @@ -33,47 +40,70 @@ const SkyboxParams = { py: PD.Text('', { label: 'Positive Y' }), pz: PD.Text('', { label: 'Positive Z' }), }, { isExpanded: true, label: 'URLs' }), - // TODO: files - }) + files: PD.Group({ + nx: PD.File({ label: 'Negative X', accept: 'image/*' }), + ny: PD.File({ label: 'Negative Y', accept: 'image/*' }), + nz: PD.File({ label: 'Negative Z', accept: 'image/*' }), + px: PD.File({ label: 'Positive X', accept: 'image/*' }), + py: PD.File({ label: 'Positive Y', accept: 'image/*' }), + pz: PD.File({ label: 'Positive Z', accept: 'image/*' }), + }, { isExpanded: true, label: 'Files' }), + }), + ...SharedParams, }; type SkyboxProps = PD.Values<typeof SkyboxParams> const ImageParams = { source: PD.MappedStatic('url', { url: PD.Text(''), - // TODO: file - }) + file: PD.File({ accept: 'image/*' }), + }), + ...SharedParams, + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), }; type ImageProps = PD.Values<typeof ImageParams> +const HorizontalGradientParams = { + topColor: PD.Color(Color(0xDDDDDD)), + bottomColor: PD.Color(Color(0xEEEEEE)), + ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), +}; + +const RadialGradientParams = { + centerColor: PD.Color(Color(0xDDDDDD)), + edgeColor: PD.Color(Color(0xEEEEEE)), + ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), +}; + export const BackgroundParams = { variant: PD.MappedStatic('off', { off: PD.EmptyGroup(), - skybox: PD.Group(SkyboxParams), - image: PD.Group(ImageParams), - horizontalGradient: PD.Group({ - topColor: PD.Color(Color(0xDDDDDD)), - bottomColor: PD.Color(Color(0xEEEEEE)), - ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), - }), - radialGradient: PD.Group({ - centerColor: PD.Color(Color(0xDDDDDD)), - edgeColor: PD.Color(Color(0xEEEEEE)), - ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), - }), - }), - opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }, { hideIf: p => p?.variant === 'off' }), + skybox: PD.Group(SkyboxParams, { isExpanded: true }), + image: PD.Group(ImageParams, { isExpanded: true }), + horizontalGradient: PD.Group(HorizontalGradientParams, { isExpanded: true }), + radialGradient: PD.Group(RadialGradientParams, { isExpanded: true }), + }, { label: 'Environment' }), }; export type BackgroundProps = PD.Values<typeof BackgroundParams> export class BackgroundPass { private renderable: BackgroundRenderable; - private skybox: ImageTexture | undefined; - private skyboxProps: SkyboxProps | undefined; + private skybox: { + texture: Texture + props: SkyboxProps + assets: Asset[] + loaded: boolean + } | undefined; - private image: ImageTexture | undefined; - private imageProps: ImageProps | undefined; + private image: { + texture: Texture + props: ImageProps + asset: Asset + loaded: boolean + } | undefined; private readonly camera = new Camera(); private readonly target = Vec3(); @@ -82,7 +112,7 @@ export class BackgroundPass { readonly texture: Texture; - constructor(private webgl: WebGLContext, width: number, height: number) { + constructor(private readonly webgl: WebGLContext, private readonly assetManager: AssetManager, width: number, height: number) { this.renderable = getBackgroundRenderable(webgl, width, height); } @@ -94,19 +124,33 @@ export class BackgroundPass { } } - private updateSkybox(camera: ICamera, props: SkyboxProps) { - const tf = this.skyboxProps?.faces; + private clearSkybox() { + if (this.skybox !== undefined) { + this.skybox.texture.destroy(); + this.skybox.assets.forEach(a => this.assetManager.release(a)); + this.skybox = undefined; + } + } + + private updateSkybox(camera: ICamera, props: SkyboxProps, onload?: (changed: boolean) => void) { + const tf = this.skybox?.props.faces; const f = props.faces.params; if (!f.nx || !f.ny || !f.nz || !f.px || !f.py || !f.pz) { - this.skybox = undefined; - this.skyboxProps = undefined; + this.clearSkybox(); + if (onload) onload(false); return; } - if (!this.skyboxProps || !tf || areSkyboxTexturePropsEqual(this.skyboxProps.faces.params, this.skyboxProps.size, props.faces.params, props.size)) { - this.skybox = getSkyboxTexture(this.webgl, props.faces.params, props.size); - ValueCell.update(this.renderable.values.tSkybox, this.skybox); + if (!this.skybox || !tf || !areSkyboxTexturePropsEqual(props.faces, this.skybox.props.faces)) { + this.clearSkybox(); + const { texture, assets } = getSkyboxTexture(this.webgl, this.assetManager, props.faces, () => { + if (this.skybox) this.skybox.loaded = true; + if (onload) onload(true); + }); + this.skybox = { texture, props: { ...props }, assets, loaded: false }; + ValueCell.update(this.renderable.values.tSkybox, texture); this.renderable.update(); - this.skyboxProps = { ...props }; + } else { + if (onload) onload(false); } if (!this.skybox) return; @@ -126,33 +170,54 @@ export class BackgroundPass { Mat4.invert(m, m); ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m); + ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity); + ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation); + ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness); ValueCell.updateIfChanged(this.renderable.values.dVariant, 'skybox'); this.renderable.update(); } - updateImage(props: ImageProps) { - if (!props.source.params) { + private clearImage() { + if (this.image !== undefined) { + this.image.texture.destroy(); + this.assetManager.release(this.image.asset); this.image = undefined; - this.imageProps = undefined; + } + } + + private updateImage(props: ImageProps, onload?: (loaded: boolean) => void) { + if (!props.source.params) { + this.clearImage(); + if (onload) onload(false); return; } - if (!this.imageProps || !this.imageProps.source.params || !props.source.params !== !this.imageProps.source.params) { - this.image = getImageTexture(this.webgl, props.source.params); - ValueCell.update(this.renderable.values.tImage, this.image); + if (!this.image || !this.image.props.source.params || !areImageTexturePropsEqual(props.source, this.image.props.source)) { + this.clearImage(); + const { texture, asset } = getImageTexture(this.webgl, this.assetManager, props.source, () => { + if (this.image) this.image.loaded = true; + if (onload) onload(true); + }); + this.image = { texture, props: { ...props }, asset, loaded: false }; + ValueCell.update(this.renderable.values.tImage, texture); this.renderable.update(); - this.imageProps = { ...props }; + } else { + if (onload) onload(false); } if (!this.image) return; + ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity); + ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation); + ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness); + ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, props.coverage === 'viewport' ? true : false); ValueCell.updateIfChanged(this.renderable.values.dVariant, 'image'); this.renderable.update(); } - updateImageScaling() { + private updateImageScaling() { const v = this.renderable.values; const [w, h] = v.uTexSize.ref.value; - const iw = this.image?.getWidth() || 0; - const ih = this.image?.getHeight() || 0; + const iw = this.image?.texture.getWidth() || 0; + const ih = this.image?.texture.getHeight() || 0; const r = w / h; const ir = iw / ih; // responsive scaling with offset @@ -170,40 +235,47 @@ export class BackgroundPass { } } - updateGradient(colorA: Color, colorB: Color, ratio: number, variant: 'horizontalGradient' | 'radialGradient') { + private updateGradient(colorA: Color, colorB: Color, ratio: number, variant: 'horizontalGradient' | 'radialGradient', viewportAdjusted: boolean) { ValueCell.update(this.renderable.values.uGradientColorA, Color.toVec3Normalized(this.renderable.values.uGradientColorA.ref.value, colorA)); ValueCell.update(this.renderable.values.uGradientColorB, Color.toVec3Normalized(this.renderable.values.uGradientColorB.ref.value, colorB)); ValueCell.updateIfChanged(this.renderable.values.uGradientRatio, ratio); + ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, viewportAdjusted); ValueCell.updateIfChanged(this.renderable.values.dVariant, variant); this.renderable.update(); } - update(camera: ICamera, props: BackgroundProps) { + update(camera: ICamera, props: BackgroundProps, onload?: (changed: boolean) => void) { if (props.variant.name === 'off') { - this.skyboxProps = undefined; + this.clearSkybox(); + this.clearImage(); + if (onload) onload(false); return; } else if (props.variant.name === 'skybox') { - this.imageProps = undefined; - this.updateSkybox(camera, props.variant.params); + this.clearImage(); + this.updateSkybox(camera, props.variant.params, onload); } else if (props.variant.name === 'image') { - this.skyboxProps = undefined; - this.updateImage(props.variant.params); + this.clearSkybox(); + this.updateImage(props.variant.params, onload); } else if (props.variant.name === 'horizontalGradient') { - this.imageProps = undefined; - this.skyboxProps = undefined; - this.updateGradient(props.variant.params.topColor, props.variant.params.bottomColor, props.variant.params.ratio, props.variant.name); + this.clearSkybox(); + this.clearImage(); + this.updateGradient(props.variant.params.topColor, props.variant.params.bottomColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false); + if (onload) onload(false); } else if (props.variant.name === 'radialGradient') { - this.imageProps = undefined; - this.skyboxProps = undefined; - this.updateGradient(props.variant.params.centerColor, props.variant.params.edgeColor, props.variant.params.ratio, props.variant.name); + this.clearSkybox(); + this.clearImage(); + this.updateGradient(props.variant.params.centerColor, props.variant.params.edgeColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false); + if (onload) onload(false); } - ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity); + + const { x, y, width, height } = camera.viewport; + ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height)); } isEnabled(props: BackgroundProps) { return !!( - (this.skyboxProps && this.skybox?.isLoaded) || - (this.imageProps && this.image?.isLoaded) || + (this.skybox && this.skybox.loaded) || + (this.image && this.image.loaded) || props.variant.name === 'horizontalGradient' || props.variant.name === 'radialGradient' ); @@ -211,8 +283,8 @@ export class BackgroundPass { private isReady() { return !!( - (this.skyboxProps && this.skybox?.isLoaded) || - (this.imageProps && this.image?.isLoaded) || + (this.skybox && this.skybox.loaded) || + (this.image && this.image.loaded) || this.renderable.values.dVariant.ref.value === 'horizontalGradient' || this.renderable.values.dVariant.ref.value === 'radialGradient' ); @@ -230,26 +302,9 @@ export class BackgroundPass { if (isTimingMode) this.webgl.timer.markEnd('BackgroundPass.render'); } - // - - static areTexturePropsEqual(propsNew: BackgroundProps, propsOld: BackgroundProps) { - if (propsNew.variant.name === 'skybox') { - if (propsOld.variant.name !== 'skybox') return false; - return areSkyboxTexturePropsEqual(propsNew.variant.params.faces.params, propsNew.variant.params.size, propsOld.variant.params.faces.params, propsOld.variant.params.size); - } else if (propsNew.variant.name === 'image') { - if (propsOld.variant.name !== 'image') return false; - return areImageTexturePropsEqual(propsNew.variant.params.source.params, propsOld.variant.params.source.params); - } else { - return true; - } - } - - static loadTexture(ctx: WebGLContext, props: BackgroundProps, onload?: () => void) { - if (props.variant.name === 'skybox') { - getSkyboxTexture(ctx, props.variant.params.faces.params, props.variant.params.size, onload); - } else if (props.variant.name === 'image') { - getImageTexture(ctx, props.variant.params.source.params, onload); - } + dispose() { + this.clearSkybox(); + this.clearImage(); } } @@ -257,55 +312,96 @@ export class BackgroundPass { const SkyboxName = 'background-skybox'; -function getSkyboxHash(faces: CubeFaces, size: number) { - return `${SkyboxName}_${faces.nx}|${faces.ny}|${faces.nz}|${faces.px}|${faces.py}|${faces.pz}|${size}`; +type CubeAssets = { [k in keyof CubeFaces]: Asset }; + +function getCubeAssets(assetManager: AssetManager, faces: SkyboxProps['faces']): CubeAssets { + if (faces.name === 'urls') { + return { + nx: Asset.getUrlAsset(assetManager, faces.params.nx), + ny: Asset.getUrlAsset(assetManager, faces.params.ny), + nz: Asset.getUrlAsset(assetManager, faces.params.nz), + px: Asset.getUrlAsset(assetManager, faces.params.px), + py: Asset.getUrlAsset(assetManager, faces.params.py), + pz: Asset.getUrlAsset(assetManager, faces.params.pz), + }; + } else { + return { + nx: faces.params.nx!, + ny: faces.params.ny!, + nz: faces.params.nz!, + px: faces.params.px!, + py: faces.params.py!, + pz: faces.params.pz!, + }; + } } -function areSkyboxTexturePropsEqual(facesA: CubeFaces, sizeA: number, facesB: CubeFaces, sizeB: number) { - return sizeA === sizeB && facesA.nx === facesB.nx && facesA.ny === facesB.ny && facesA.nz === facesB.nz && facesA.px === facesB.px && facesA.py === facesB.py && facesA.pz === facesB.pz; +function getCubeFaces(assetManager: AssetManager, cubeAssets: CubeAssets): CubeFaces { + const resolve = (asset: Asset) => { + return assetManager.resolve(asset, 'binary').run().then(a => new Blob([a.data])); + }; + + return { + nx: resolve(cubeAssets.nx), + ny: resolve(cubeAssets.ny), + nz: resolve(cubeAssets.nz), + px: resolve(cubeAssets.px), + py: resolve(cubeAssets.py), + pz: resolve(cubeAssets.pz), + }; } -function getSkyboxTexture(ctx: WebGLContext, faces: CubeFaces, size: number, onload?: () => void): ImageTexture { - const hash = getSkyboxHash(faces, size); - if (!ctx.namedTextures[hash]) { - ctx.namedTextures[hash] = createCubeTexture(ctx.gl, faces, size, onload); - } else if (onload) { - onload(); +function getSkyboxHash(faces: SkyboxProps['faces']) { + if (faces.name === 'urls') { + return `${SkyboxName}_${faces.params.nx}|${faces.params.ny}|${faces.params.nz}|${faces.params.px}|${faces.params.py}|${faces.params.pz}`; + } else { + return `${SkyboxName}_${faces.params.nx?.id}|${faces.params.ny?.id}|${faces.params.nz?.id}|${faces.params.px?.id}|${faces.params.py?.id}|${faces.params.pz?.id}`; } - return ctx.namedTextures[hash] as ImageTexture; +} + +function areSkyboxTexturePropsEqual(facesA: SkyboxProps['faces'], facesB: SkyboxProps['faces']) { + return getSkyboxHash(facesA) === getSkyboxHash(facesB); +} + +function getSkyboxTexture(ctx: WebGLContext, assetManager: AssetManager, faces: SkyboxProps['faces'], onload?: () => void): { texture: Texture, assets: Asset[] } { + const cubeAssets = getCubeAssets(assetManager, faces); + const cubeFaces = getCubeFaces(assetManager, cubeAssets); + const assets = [cubeAssets.nx, cubeAssets.ny, cubeAssets.nz, cubeAssets.px, cubeAssets.py, cubeAssets.pz]; + const texture = ctx.resources.cubeTexture(cubeFaces, false, onload); + return { texture, assets }; } // const ImageName = 'background-image'; -function getImageHash(source: string) { - return `${ImageName}_${source}`; +function getImageHash(source: ImageProps['source']) { + if (source.name === 'url') { + return `${ImageName}_${source.params}`; + } else { + return `${ImageName}_${source.params?.id}`; + } } -function areImageTexturePropsEqual(sourceA: string, sourceB: string) { - return sourceA === sourceB; +function areImageTexturePropsEqual(sourceA: ImageProps['source'], sourceB: ImageProps['source']) { + return getImageHash(sourceA) === getImageHash(sourceB); } -function getImageTexture(ctx: WebGLContext, source: string, onload?: () => void): ImageTexture { - const hash = getImageHash(source); - if (!ctx.namedTextures[hash]) { - const texture = { - ...createTexture(ctx.gl, ctx.extensions, 'image-uint8', 'rgba', 'ubyte', 'linear'), - isLoaded: false, - }; - const img = new Image(); - img.onload = () => { - texture.load(img); - texture.isLoaded = true; - onload?.(); - }; - img.src = source; - ctx.namedTextures[hash] = texture; - } else if (onload) { - onload(); - } - return ctx.namedTextures[hash] as ImageTexture; +function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source: ImageProps['source'], onload?: () => void): { texture: Texture, asset: Asset } { + const texture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); + const img = new Image(); + img.onload = () => { + texture.load(img); + onload?.(); + }; + const asset = source.name === 'url' + ? Asset.getUrlAsset(assetManager, source.params) + : source.params!; + assetManager.resolve(asset, 'binary').run().then(a => { + const blob = new Blob([a.data]); + img.src = URL.createObjectURL(blob); + }); + return { texture, asset }; } // @@ -319,11 +415,15 @@ const BackgroundSchema = { uImageScale: UniformSpec('v2'), uImageOffset: UniformSpec('v2'), uTexSize: UniformSpec('v2'), + uViewport: UniformSpec('v4'), + uViewportAdjusted: UniformSpec('b'), uViewDirectionProjectionInverse: UniformSpec('m4'), uGradientColorA: UniformSpec('v3'), uGradientColorB: UniformSpec('v3'), uGradientRatio: UniformSpec('f'), uOpacity: UniformSpec('f'), + uSaturation: UniformSpec('f'), + uLightness: UniformSpec('f'), dVariant: DefineSpec('string', ['skybox', 'image', 'verticalGradient', 'horizontalGradient', 'radialGradient']), }; const SkyboxShaderCode = ShaderCode('background', background_vert, background_frag); @@ -339,11 +439,15 @@ function getBackgroundRenderable(ctx: WebGLContext, width: number, height: numbe uImageScale: ValueCell.create(Vec2()), uImageOffset: ValueCell.create(Vec2()), uTexSize: ValueCell.create(Vec2.create(width, height)), + uViewport: ValueCell.create(Vec4()), + uViewportAdjusted: ValueCell.create(true), uViewDirectionProjectionInverse: ValueCell.create(Mat4()), uGradientColorA: ValueCell.create(Vec3()), uGradientColorB: ValueCell.create(Vec3()), uGradientRatio: ValueCell.create(0.5), uOpacity: ValueCell.create(1), + uSaturation: ValueCell.create(0), + uLightness: ValueCell.create(0), dVariant: ValueCell.create('skybox'), }; diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index 86a4a44eed0894f285f98bbd9d7eb0deba33ef3d..491a4ab225a8e01bc6eba3a0bfb5021dac47958d 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/src/mol-canvas3d/passes/draw.ts @@ -21,6 +21,7 @@ import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './pos import { MarkingPass, MarkingProps } from './marking'; import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util'; import { isTimingMode } from '../../mol-util/debug'; +import { AssetManager } from '../../mol-util/assets'; type Props = { postprocessing: PostprocessingProps; @@ -59,7 +60,7 @@ export class DrawPass { return !!this.wboit?.supported; } - constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, width: number, height: number, enableWboit: boolean) { const { extensions, resources, isWebGL2 } = webgl; this.drawTarget = createNullRenderTarget(webgl.gl); @@ -78,7 +79,7 @@ export class DrawPass { this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined; this.marking = new MarkingPass(webgl, width, height); - this.postprocessing = new PostprocessingPass(webgl, this); + this.postprocessing = new PostprocessingPass(webgl, assetManager, this); this.antialiasing = new AntialiasingPass(webgl, this); this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture); diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts index 68acfa4c4b6977b36669d0b3a7afdbc6b01b96a2..b8e8e440e26d4cafdc5ecdf3e63556dc099a9fda 100644 --- a/src/mol-canvas3d/passes/image.ts +++ b/src/mol-canvas3d/passes/image.ts @@ -18,6 +18,7 @@ import { PixelData } from '../../mol-util/image'; import { Helper } from '../helper/helper'; import { CameraHelper, CameraHelperParams } from '../helper/camera-helper'; import { MarkingParams } from './marking'; +import { AssetManager } from '../../mol-util/assets'; export const ImageParams = { transparentBackground: PD.Boolean(false), @@ -47,10 +48,10 @@ export class ImagePass { get width() { return this._width; } get height() { return this._height; } - constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) { this.props = { ...PD.getDefaultValues(ImageParams), ...props }; - this.drawPass = new DrawPass(webgl, 128, 128, enableWboit); + this.drawPass = new DrawPass(webgl, assetManager, 128, 128, enableWboit); this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass); this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass); @@ -63,6 +64,14 @@ export class ImagePass { this.setSize(1024, 768); } + async updateBackground() { + return new Promise<void>(resolve => { + this.drawPass.postprocessing.background.update(this.camera, this.props.postprocessing.background, () => { + resolve(); + }); + }); + } + setSize(width: number, height: number) { if (width === this._width && height === this._height) return; diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts index 208795e33bb2af60d8966f2f857fb87dc594087a..117bb6ef0526f68074b40bf08b0f437d88c5ba88 100644 --- a/src/mol-canvas3d/passes/passes.ts +++ b/src/mol-canvas3d/passes/passes.ts @@ -8,15 +8,16 @@ import { DrawPass } from './draw'; import { PickPass } from './pick'; import { MultiSamplePass } from './multi-sample'; import { WebGLContext } from '../../mol-gl/webgl/context'; +import { AssetManager } from '../../mol-util/assets'; export class Passes { readonly draw: DrawPass; readonly pick: PickPass; readonly multiSample: MultiSamplePass; - constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) { const { gl } = webgl; - this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false); + this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false); this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25); this.multiSample = new MultiSamplePass(webgl, this.draw); } diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index 7e63cfed87f3298b8b5ffbf96b4563105a9d67fe..c38b5ca52aae2d5ccae9ef1873493290a2bb3338 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/src/mol-canvas3d/passes/postprocessing.ts @@ -29,6 +29,7 @@ import { FxaaParams, FxaaPass } from './fxaa'; import { SmaaParams, SmaaPass } from './smaa'; import { isTimingMode } from '../../mol-util/debug'; import { BackgroundParams, BackgroundPass } from './background'; +import { AssetManager } from '../../mol-util/assets'; const OutlinesSchema = { ...QuadSchema, @@ -275,7 +276,7 @@ export const PostprocessingParams = { smaa: PD.Group(SmaaParams), off: PD.Group({}) }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }), - background: PD.Group(BackgroundParams, { isExpanded: true }), + background: PD.Group(BackgroundParams, { isFlat: true }), }; export type PostprocessingProps = PD.Values<typeof PostprocessingParams> @@ -323,7 +324,7 @@ export class PostprocessingPass { private readonly bgColor = Vec3(); readonly background: BackgroundPass; - constructor(private webgl: WebGLContext, private drawPass: DrawPass) { + constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) { const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass; const width = colorTarget.getWidth(); const height = colorTarget.getHeight(); @@ -374,7 +375,7 @@ export class PostprocessingPass { this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical'); this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture); - this.background = new BackgroundPass(webgl, width, height); + this.background = new BackgroundPass(webgl, assetManager, width, height); } setSize(width: number, height: number) { diff --git a/src/mol-gl/shader/background.frag.ts b/src/mol-gl/shader/background.frag.ts index ea5266b97b093f4eaf8fb12498409ae2b15a4c1e..a764a9ad8237ec635bb53ad45c7bb7e08f297c4f 100644 --- a/src/mol-gl/shader/background.frag.ts +++ b/src/mol-gl/shader/background.frag.ts @@ -6,10 +6,16 @@ precision mediump sampler2D; #if defined(dVariant_skybox) uniform samplerCube tSkybox; uniform mat4 uViewDirectionProjectionInverse; + uniform float uOpacity; + uniform float uSaturation; + uniform float uLightness; #elif defined(dVariant_image) uniform sampler2D tImage; uniform vec2 uImageScale; uniform vec2 uImageOffset; + uniform float uOpacity; + uniform float uSaturation; + uniform float uLightness; #elif defined(dVariant_horizontalGradient) || defined(dVariant_radialGradient) uniform vec3 uGradientColorA; uniform vec3 uGradientColorB; @@ -17,7 +23,8 @@ precision mediump sampler2D; #endif uniform vec2 uTexSize; -uniform float uOpacity; +uniform vec4 uViewport; +uniform bool uViewportAdjusted; varying vec4 vPosition; // TODO: add as general pp option to remove banding? @@ -28,22 +35,50 @@ vec3 ScreenSpaceDither(vec2 vScreenPos) { return vDither.rgb / 255.0; } +vec3 saturateColor(vec3 c, float amount) { + // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance + const vec3 W = vec3(0.2125, 0.7154, 0.0721); + vec3 intensity = vec3(dot(c, W)); + return mix(intensity, c, 1.0 + amount); +} + +vec3 lightenColor(vec3 c, float amount) { + return c + amount; +} + void main() { #if defined(dVariant_skybox) vec4 t = uViewDirectionProjectionInverse * vPosition; gl_FragColor = textureCube(tSkybox, normalize(t.xyz / t.w)); gl_FragColor.a = uOpacity; + gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness); #elif defined(dVariant_image) - vec2 coords = (gl_FragCoord.xy / uImageScale) + uImageOffset; + vec2 coords; + if (uViewportAdjusted) { + coords = ((gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uImageScale) + uImageOffset; + } else { + coords = (gl_FragCoord.xy / uImageScale) + uImageOffset; + } gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y)); gl_FragColor.a = uOpacity; + gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness); #elif defined(dVariant_horizontalGradient) - float d = (gl_FragCoord.y / uTexSize.y) + 1.0 - (uGradientRatio * 2.0); - gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, clamp(d, 0.0, 1.0)), uOpacity); + float d; + if (uViewportAdjusted) { + d = ((gl_FragCoord.y - uViewport.y) * (uTexSize.y / uViewport.w) / uTexSize.y) + 1.0 - (uGradientRatio * 2.0); + } else { + d = (gl_FragCoord.y / uTexSize.y) + 1.0 - (uGradientRatio * 2.0); + } + gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, clamp(d, 0.0, 1.0)), 1.0); gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy); #elif defined(dVariant_radialGradient) - float d = distance(vec2(0.5), gl_FragCoord.xy / uTexSize) + uGradientRatio - 0.5; - gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, 1.0 - clamp(d, 0.0, 1.0)), uOpacity); + float d; + if (uViewportAdjusted) { + d = distance(vec2(0.5), (gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uTexSize) + uGradientRatio - 0.5; + } else { + d = distance(vec2(0.5), gl_FragCoord.xy / uTexSize) + uGradientRatio - 0.5; + } + gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, 1.0 - clamp(d, 0.0, 1.0)), 1.0); gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy); #endif } diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index c3cd962a4f42716371db39688dcd4c66cd46ac22..83c80e48381ea99b66727e24094d7240ae2973f3 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -164,6 +164,7 @@ function createStats() { renderbuffer: 0, shader: 0, texture: 0, + cubeTexture: 0, vertexArray: 0, }, diff --git a/src/mol-gl/webgl/resources.ts b/src/mol-gl/webgl/resources.ts index 4dd175cc7788bfdffd80614b8a56add7d2f7a336..3e2e2d370bfc771499cf8b5b7641f5ab25b2ce52 100644 --- a/src/mol-gl/webgl/resources.ts +++ b/src/mol-gl/webgl/resources.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -17,7 +17,7 @@ import { hashString, hashFnv32a } from '../../mol-data/util'; import { DefineValues, ShaderCode } from '../shader-code'; import { RenderableSchema } from '../renderable/schema'; import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer'; -import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture } from './texture'; +import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture, CubeFaces, createCubeTexture } from './texture'; import { VertexArray, createVertexArray } from './vertex-array'; function defineValueHash(v: boolean | number | string): number { @@ -59,6 +59,7 @@ export interface WebGLResources { renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer shader: (type: ShaderType, source: string) => Shader texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture, + cubeTexture: (faces: CubeFaces, mipaps: boolean, onload?: () => void) => Texture, vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray, getByteCounts: () => ByteCounts @@ -76,6 +77,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats renderbuffer: new Set<Resource>(), shader: new Set<Resource>(), texture: new Set<Resource>(), + cubeTexture: new Set<Resource>(), vertexArray: new Set<Resource>(), }; @@ -137,6 +139,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => { return wrap('texture', createTexture(gl, extensions, kind, format, type, filter)); }, + cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => { + return wrap('cubeTexture', createCubeTexture(gl, faces, mipmaps, onload)); + }, vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => { return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer)); }, @@ -146,6 +151,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats sets.texture.forEach(r => { texture += (r as Texture).getByteCount(); }); + sets.cubeTexture.forEach(r => { + texture += (r as Texture).getByteCount(); + }); let attribute = 0; sets.attribute.forEach(r => { diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts index 362c559104e489c0ae174d67fb47e2466b272f63..f2371edcf1eb8265a99ce3d1da94c43612453d8c 100644 --- a/src/mol-gl/webgl/texture.ts +++ b/src/mol-gl/webgl/texture.ts @@ -11,7 +11,7 @@ import { RenderableSchema } from '../renderable/schema'; import { idFactory } from '../../mol-util/id-factory'; import { Framebuffer } from './framebuffer'; import { isWebGL2, GLRenderingContext } from './compat'; -import { ValueOf } from '../../mol-util/type-helpers'; +import { isPromiseLike, ValueOf } from '../../mol-util/type-helpers'; import { WebGLExtensions } from './extensions'; import { objectForEach } from '../../mol-util/object'; @@ -424,14 +424,10 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture: // -export interface ImageTexture extends Texture { - readonly isLoaded: boolean; -} - export type CubeSide = 'nx' | 'ny' | 'nz' | 'px' | 'py' | 'pz'; export type CubeFaces = { - [k in CubeSide]: string; + [k in CubeSide]: string | File | Promise<Blob>; } export function getCubeTarget(gl: GLRenderingContext, side: CubeSide): number { @@ -445,50 +441,61 @@ export function getCubeTarget(gl: GLRenderingContext, side: CubeSide): number { } } -export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, size: number, onload?: () => void): ImageTexture { +export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipmaps: boolean, onload?: () => void): Texture { const target = gl.TEXTURE_CUBE_MAP; const filter = gl.LINEAR; const internalFormat = gl.RGBA; const format = gl.RGBA; const type = gl.UNSIGNED_BYTE; - const width = size; - const height = size; + let size = 0; const texture = gl.createTexture(); gl.bindTexture(target, texture); let loadedCount = 0; - objectForEach(faces, (url, side) => { + objectForEach(faces, (source, side) => { + if (!source) return; + const level = 0; const cubeTarget = getCubeTarget(gl, side as CubeSide); - gl.texImage2D(cubeTarget, level, internalFormat, width, height, 0, format, type, null); - if (!url) return; - const image = new Image(); - image.src = url; + if (source instanceof File) { + image.src = URL.createObjectURL(source); + } else if (isPromiseLike(source)) { + source.then(blob => { + image.src = URL.createObjectURL(blob); + }); + } else { + image.src = source; + } image.addEventListener('load', () => { + if (size === 0) size = image.width; + + gl.texImage2D(cubeTarget, level, internalFormat, size, size, 0, format, type, null); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.bindTexture(target, texture); gl.texImage2D(cubeTarget, level, internalFormat, format, type, image); - gl.generateMipmap(target); + loadedCount += 1; - if (loadedCount === 6) { - loaded = true; - if (!destroyed && onload) onload(); + if (loadedCount === 6 && !destroyed) { + if (mipmaps) { + gl.generateMipmap(target); + gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + } else { + gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter); + } + gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter); + if (onload) onload(); } }); }); - gl.generateMipmap(target); - gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); - gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter); let destroyed = false; - let loaded = false; return { id: getNextTextureId(), @@ -498,14 +505,12 @@ export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, size type, filter, - get isLoaded() { - return loaded; - }, - - getWidth: () => width, - getHeight: () => height, + getWidth: () => size, + getHeight: () => size, getDepth: () => 0, - getByteCount: () => getByteCount('rgba', 'ubyte', width, height, 0) * 6, + getByteCount: () => { + return getByteCount('rgba', 'ubyte', size, size, 0) * 6 * (mipmaps ? 2 : 1); + }, define: () => {}, load: () => {}, diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx index 73f819331fbc1188e0d594da430e1fb3ec6af0be..122e11f8a64fa9d987db949223698ec55fc66771 100644 --- a/src/mol-plugin-ui/viewport/simple-settings.tsx +++ b/src/mol-plugin-ui/viewport/simple-settings.tsx @@ -8,8 +8,10 @@ import { produce } from 'immer'; import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d'; import { PluginCommands } from '../../mol-plugin/commands'; +import { PluginConfig } from '../../mol-plugin/config'; import { StateTransform } from '../../mol-state'; import { Color } from '../../mol-util/color'; +import { deepClone } from '../../mol-util/object'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { ParamMapping } from '../../mol-util/param-mapping'; import { Mutable } from '../../mol-util/type-helpers'; @@ -50,7 +52,8 @@ const SimpleSettingsParams = { camera: Canvas3DParams.camera, background: PD.Group({ color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }), - transparent: PD.Boolean(false) + transparent: PD.Boolean(false), + style: Canvas3DParams.postprocessing.params.background, }, { pivot: 'color' }), lighting: PD.Group({ occlusion: Canvas3DParams.postprocessing.params.occlusion, @@ -75,6 +78,13 @@ const SimpleSettingsMapping = ParamMapping({ if (controls.left !== 'none') options.push(['left', LayoutOptions.left]); params.layout.options = options; } + const bgStyles = ctx.config.get(PluginConfig.Background.Styles) || []; + if (bgStyles.length > 0) { + Object.assign(params.background.params.style, { + presets: deepClone(bgStyles), + isFlat: false, // so the presets menu is shown + }); + } return params; }, target(ctx: PluginUIContext) { @@ -97,7 +107,8 @@ const SimpleSettingsMapping = ParamMapping({ camera: canvas.camera, background: { color: renderer.backgroundColor, - transparent: canvas.transparentBackground + transparent: canvas.transparentBackground, + style: canvas.postprocessing.background, }, lighting: { occlusion: canvas.postprocessing.occlusion, @@ -117,6 +128,7 @@ const SimpleSettingsMapping = ParamMapping({ canvas.renderer.backgroundColor = s.background.color; canvas.postprocessing.occlusion = s.lighting.occlusion; canvas.postprocessing.outline = s.lighting.outline; + canvas.postprocessing.background = s.background.style; canvas.cameraFog = s.lighting.fog; canvas.cameraClipping = { radius: s.clipping.radius, diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts index b70305fb7f6773cc470aca8bc9733a15db101d87..2abb40578fafff36078c892e11fc9247f74cc9e0 100644 --- a/src/mol-plugin/config.ts +++ b/src/mol-plugin/config.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2022 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> @@ -12,6 +12,7 @@ import { EmdbDownloadProvider } from '../mol-plugin-state/actions/volume'; import { StructureRepresentationPresetProvider } from '../mol-plugin-state/builder/structure/representation-preset'; import { PluginFeatureDetection } from './features'; import { SaccharideCompIdMapType } from '../mol-model/structure/structure/carbohydrates/constants'; +import { BackgroundProps } from '../mol-canvas3d/passes/background'; export class PluginConfigItem<T = any> { toString() { return this.key; } @@ -65,6 +66,9 @@ export const PluginConfig = { DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'), DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }), SaccharideCompIdMapType: item<SaccharideCompIdMapType>('structure.saccharide-comp-id-map-type', 'default'), + }, + Background: { + Styles: item<[BackgroundProps, string][]>('background.styles', []), } }; diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 29ec1c03d65a88bbff9c669fd86e3eed0a5901b4..405087080f0c118dece7b9c2ec541134ddcf23d0 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -201,7 +201,7 @@ export class PluginContext { const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1; const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false; const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false; - (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 }); + (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, this.managers.asset, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 }); } (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!); this.canvas3dInit.next(true); diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts index 261ae3708d95a5c428b162b35ff69242d306c30c..97368fde642536abe397a736829216e82cbee236 100644 --- a/src/mol-plugin/util/viewport-screenshot.ts +++ b/src/mol-plugin/util/viewport-screenshot.ts @@ -309,7 +309,9 @@ class ViewportScreenshotHelper extends PluginComponent { if (width <= 0 || height <= 0) return; await ctx.update('Rendering image...'); - const imageData = this.imagePass.getImageData(width, height, viewport); + const pass = this.imagePass; + await pass.updateBackground(); + const imageData = pass.getImageData(width, height, viewport); await ctx.update('Encoding image...'); const canvas = this.canvas; diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts index 5f585b77984a400c5b3ceaa0979db8dbe1ec8a52..b9ef65cb60e415ff238cd5fe8724cebc13df73b5 100644 --- a/src/tests/browser/marching-cubes.ts +++ b/src/tests/browser/marching-cubes.ts @@ -22,6 +22,7 @@ import { Representation } from '../../mol-repr/representation'; import { computeMarchingCubesMesh } from '../../mol-geo/util/marching-cubes/algorithm'; import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -31,7 +32,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { renderer: { backgroundColor: ColorNames.white }, camera: { mode: 'orthographic' } })); diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts index d4996f777d1ffcb2865e951c41935c296d7aa03d..137ca2a69f9d71023fd9d3f5d4ba3af469b6bcf6 100644 --- a/src/tests/browser/render-lines.ts +++ b/src/tests/browser/render-lines.ts @@ -15,6 +15,7 @@ import { Color } from '../../mol-util/color'; import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -24,7 +25,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function linesRepr() { diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts index 12861bad246a92f054407259eef86686d45cfbde..e639b931e7dc826a5f7ab86808ed74cc8da1c8ce 100644 --- a/src/tests/browser/render-mesh.ts +++ b/src/tests/browser/render-mesh.ts @@ -17,6 +17,7 @@ import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { Torus } from '../../mol-geo/primitive/torus'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -26,7 +27,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function meshRepr() { diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index cf83c1c9a4f8518ecf84416e804b07e594ee5552..184d273939372d42893fa6086f06b32792266cd0 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -19,6 +19,7 @@ import { Sphere } from '../../mol-geo/primitive/sphere'; import { ColorNames } from '../../mol-util/color/names'; import { Shape } from '../../mol-model/shape'; import { ShapeRepresentation } from '../../mol-repr/shape/representation'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -28,6 +29,8 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); +const assetManager = new AssetManager(); + const info = document.createElement('div'); info.style.position = 'absolute'; info.style.fontFamily = 'sans-serif'; @@ -38,7 +41,7 @@ info.style.color = 'white'; parent.appendChild(info); let prevReprLoci = Representation.Loci.Empty; -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); canvas3d.input.move.subscribe(({ x, y }) => { const pickingId = canvas3d.identify(x, y)?.id; diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts index 439429fe35d6efec99e0ec225ab266b48e98b8a0..ed4e92ae278411bd11b155dbb3cc23fd38fd6b67 100644 --- a/src/tests/browser/render-spheres.ts +++ b/src/tests/browser/render-spheres.ts @@ -13,6 +13,7 @@ import { Color } from '../../mol-util/color'; import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -22,7 +23,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function spheresRepr() { diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts index 07e23ee4918d7a8698ceb01cc843ec62a4ceb614..634ccd8adeb7366d499ef77d0afffc6444961317 100644 --- a/src/tests/browser/render-structure.ts +++ b/src/tests/browser/render-structure.ts @@ -37,7 +37,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); const info = document.createElement('div'); @@ -123,7 +125,7 @@ function getMembraneOrientationRepr() { } async function init() { - const ctx = { runtime: SyncRuntimeContext, assetManager: new AssetManager() }; + const ctx = { runtime: SyncRuntimeContext, assetManager }; const cif = await downloadFromPdb('3pqr'); const models = await getModels(cif); diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts index c25a45fa195c9d1e3d8eaf1f0b32db0a0058bcbe..b1b0a33a09346f88270be7bf3138ed366b0e4ece 100644 --- a/src/tests/browser/render-text.ts +++ b/src/tests/browser/render-text.ts @@ -15,6 +15,7 @@ import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder'; import { createRenderObject } from '../../mol-gl/render-object'; import { Spheres } from '../../mol-geo/geometry/spheres/spheres'; import { resizeCanvas } from '../../mol-canvas3d/util'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -24,7 +25,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function textRepr() { diff --git a/webpack.config.common.js b/webpack.config.common.js index 491eb8d551226831574049bcd850af99c1b939f7..380e1cc3e871daf21e71553e7b0efe452aacd3cc 100644 --- a/webpack.config.common.js +++ b/webpack.config.common.js @@ -30,7 +30,11 @@ const sharedConfig = { { loader: 'css-loader', options: { sourceMap: false } }, { loader: 'sass-loader', options: { sourceMap: false } }, ] - } + }, + { + test: /\.(jpg)$/i, + type: 'asset/resource', + }, ] }, plugins: [ @@ -76,7 +80,7 @@ function createEntry(src, outFolder, outFilename, isNode) { function createEntryPoint(name, dir, out, library) { return { entry: path.resolve(__dirname, `lib/${dir}/${name}.js`), - output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd' }, + output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd', assetModuleFilename: 'images/[hash][ext][query]', 'publicPath': '' }, ...sharedConfig }; }