Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View independent transparency override #2692

Merged
merged 12 commits into from
Nov 12, 2021
76 changes: 54 additions & 22 deletions core/common/src/FeatureSymbology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,55 @@ function copyIdSetToUint32Set(dst: Id64.Uint32Set, src: Iterable<string>): void

// cspell:ignore subcat subcats

/** Properties used to initialize a [[FeatureAppearance]].
/** JSON representation of a [[FeatureAppearance]].
* @public
*/
export interface FeatureAppearanceProps {
/** The color of the Feature */
/** @see [[FeatureAppearance.rgb]]. */
rgb?: RgbColorProps;
/** The line weight in pixels as an integer in [1, 31] */
/** @see [[FeatureAppearance.weight]]. */
weight?: number;
/** The transparency in the range [0.0, 1.0] where 0 indicates fully opaque and 1 indicates fully transparent. */
/** @see [[FeatureAppearance.transparency]]. */
transparency?: number;
/** The pixel pattern used to draw lines. */
/** @see [[FeatureAppearance.viewDependentTransparency]]. */
viewDependentTransparency?: true;
/** @see [[FeatureAppearance.linePixels]]. */
linePixels?: LinePixels;
/** If true, ignore the [[RenderMaterial]] associated with surfaces. */
ignoresMaterial?: true | undefined;
/** If true, the associated [[Feature]] will not be drawn when using [Viewport.readPixels]($frontend). */
nonLocatable?: true | undefined;
/** If true, the associated [[Feature]] will be emphasized. Emphasized features are rendered using the [[Hilite.Settings]] defined by [Viewport.emphasisSettings]($frontend). */
emphasized?: true | undefined;
/** @see [[FeatureAppearance.ignoresMaterial]]. */
ignoresMaterial?: true;
/** @see [[FeatureAppearance.nonLocatable]]. */
nonLocatable?: true;
/** @see [[FeatureAppearance.emphasized]]. */
emphasized?: true;
}

/** Defines overrides for selected aspects of a [[Feature]]'s symbology.
* Any member defined in the appearance overrides that aspect of symbology for all [[Feature]]s to which the appearance is applied.
* @see [[FeatureOverrides]] to customize the appearance of multiple features.
* @public
*/
export class FeatureAppearance implements FeatureAppearanceProps {
export class FeatureAppearance {
/** Overrides the feature's color. */
public readonly rgb?: RgbColor;
/** The width of lines and edges in pixels as an integer in [1, 31]. */
public readonly weight?: number;
/** The transparency in the range [0, 1] where 0 indicates fully opaque and 1 indicates fully transparent.
* @see [[viewDependentTransparency]] for details on how this override interacts with the [DisplayStyle]($backend).
*/
public readonly transparency?: number;
/** The pixel pattern applied to lines and edges. */
public readonly linePixels?: LinePixels;
public readonly ignoresMaterial?: true | undefined;
public readonly nonLocatable?: true | undefined;
public readonly emphasized?: true | undefined;
/** If true, don't apply the [[RenderMaterial]] to the feature's surfaces. */
public readonly ignoresMaterial?: true;
/** If true, the feature will not be drawn when using [Viewport.readPixels]($frontend), meaning [Tool]($frontend)s will not be able to interact with it. */
public readonly nonLocatable?: true;
/** If true, the feature will be rendered using the [[Hilite.Settings]] defined by [Viewport.emphasisSettings]($frontend) to make it stand out. */
public readonly emphasized?: true;
/** If true, then [[transparency]] will only be applied if [[ViewFlags.transparency]] is enabled and the current [[RenderMode]] supports transparency.
* Default: false, meaning the transparency will be applied regardless of view flags or render mode.
* This property has no effect if [[transparency]] is `undefined`.
*/
public readonly viewDependentTransparency?: true;

/** An appearance that overrides nothing. */
public static readonly defaults = new FeatureAppearance({});
Expand All @@ -80,24 +96,30 @@ export class FeatureAppearance implements FeatureAppearanceProps {
/** Create a FeatureAppearance that overrides the RGB and transparency.
* The appearance's transparency is derived from the transparency component of the ColorDef.
*/
public static fromRgba(color: ColorDef): FeatureAppearance {
public static fromRgba(color: ColorDef, viewDependentTransparency = false): FeatureAppearance {
return this.fromJSON({
rgb: RgbColor.fromColorDef(color),
transparency: color.colors.t / 255,
viewDependentTransparency: viewDependentTransparency ? true : undefined,
});
}
/** Create a FeatureAppearance that overrides only the transparency */
public static fromTransparency(transparencyValue: number): FeatureAppearance {
return this.fromJSON({ transparency: transparencyValue });
public static fromTransparency(transparencyValue: number, viewDependent = false): FeatureAppearance {
return this.fromJSON({
transparency: transparencyValue,
viewDependentTransparency: viewDependent ? true : undefined,
});
}

/** Create a FeatureAppearance with overrides corresponding to those defined by the supplied SubCategoryOverride. */
/** Create a FeatureAppearance with overrides corresponding to those defined by the supplied SubCategoryOverride.
* @note Subcategory overrides set [[viewDependentTransparency]] to `true`.
*/
public static fromSubCategoryOverride(ovr: SubCategoryOverride): FeatureAppearance {
const rgb = undefined !== ovr.color ? RgbColor.fromColorDef(ovr.color) : undefined;
const transparency = ovr.transparency;
const weight = ovr.weight;
const ignoresMaterial = undefined !== ovr.material && Id64.isValid(ovr.material) ? true : undefined;
return this.fromJSON({ rgb, transparency, weight, ignoresMaterial });
return this.fromJSON({ rgb, transparency, weight, ignoresMaterial, viewDependentTransparency: true });
}

/** Returns true if this appearance does not override any aspects of symbology. */
Expand Down Expand Up @@ -128,7 +150,8 @@ export class FeatureAppearance implements FeatureAppearanceProps {
&& this.linePixels === other.linePixels
&& this.ignoresMaterial === other.ignoresMaterial
&& this.nonLocatable === other.nonLocatable
&& this.emphasized === other.emphasized;
&& this.emphasized === other.emphasized
&& this.viewDependentTransparency === other.viewDependentTransparency;
}

public toJSON(): FeatureAppearanceProps {
Expand All @@ -139,8 +162,11 @@ export class FeatureAppearance implements FeatureAppearanceProps {
if (undefined !== this.weight)
props.weight = this.weight;

if (undefined !== this.transparency)
if (undefined !== this.transparency) {
props.transparency = this.transparency;
if (this.viewDependentTransparency)
props.viewDependentTransparency = true;
}

if (undefined !== this.linePixels)
props.linePixels = this.linePixels;
Expand Down Expand Up @@ -198,6 +224,9 @@ export class FeatureAppearance implements FeatureAppearanceProps {
if (undefined === props.nonLocatable && this.nonLocatable) props.nonLocatable = true;
if (undefined === props.emphasized && this.emphasized) props.emphasized = true;

if (undefined !== props.transparency && this.viewDependentTransparency)
props.viewDependentTransparency = true;

return FeatureAppearance.fromJSON(props);
}

Expand All @@ -214,6 +243,9 @@ export class FeatureAppearance implements FeatureAppearanceProps {
this.weight = Math.max(1, Math.min(this.weight, 32));

if (undefined !== this.transparency) {
if (props.viewDependentTransparency)
this.viewDependentTransparency = true;

this.transparency = Math.max(0, Math.min(this.transparency, 1));

// Fix up rounding errors...
Expand Down
38 changes: 38 additions & 0 deletions core/common/src/test/FeatureSymbology.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,44 @@ describe("FeatureAppearance", () => {
test({ transp: 0.5 }, { transparency: 0.5 });
test({ transp: 1.0 }, { transparency: 1.0 });
});

it("view-dependent transparency", () => {
it("to and from JSON", () => {
function test(appProps: FeatureAppearanceProps, expectViewDependent: boolean): void {
const expected = expectViewDependent ? true : undefined;
const app = FeatureAppearance.fromJSON(appProps);
expect(app.viewDependentTransparency).to.equal(expected);
expect(app.toJSON().viewDependentTransparency).to.equal(expected);
}

test({ }, false);
test({ transparency: undefined }, false);
test({ transparency: 1 }, false);
test({ transparency: 0 }, false );

test({ transparency: 1, viewDependentTransparency: true }, true);
test({ transparency: 0, viewDependentTransparency: true }, true);

test({ viewDependentTransparency: true }, false);
test({ transparency: undefined, viewDependentTransparency: true }, false);
});

it("from subcategory override", () => {
function test(ovrProps: SubCategoryAppearance.Props, expectViewDependent: boolean): void {
const expected = expectViewDependent ? true : undefined;
const ovr = SubCategoryOverride.fromJSON(ovrProps);
const app = FeatureAppearance.fromSubCategoryOverride(ovr);
expect(app.viewDependentTransparency).to.equal(expected);
expect(app.toJSON().viewDependentTransparency).to.equal(expected);
}

test({ transp: 0.5 }, true);
test({ transp: 0 }, true);
test({ transp: undefined }, false);
test({ }, false);
test({ color: ColorDef.blue.toJSON() }, false);
});
});
});

describe("FeatureOverrides", () => {
Expand Down
13 changes: 10 additions & 3 deletions core/frontend/src/render/webgl/FeatureOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class FeatureOverrides implements WebGLDisposable {
private _anyOverridden = true;
private _allHidden = true;
private _anyTranslucent = true;
private _anyViewIndependentTranslucent = true;
private _anyOpaque = true;
private _anyHilited = true;
private _lutParams = new Float32Array(2);
Expand All @@ -83,6 +84,7 @@ export class FeatureOverrides implements WebGLDisposable {
public get anyOverridden() { return this._anyOverridden; }
public get allHidden() { return this._allHidden; }
public get anyTranslucent() { return this._anyTranslucent; }
public get anyViewIndependentTranslucent() { return this._anyViewIndependentTranslucent; }
public get anyOpaque() { return this._anyOpaque; }
public get anyHilited() { return this._anyHilited; }

Expand Down Expand Up @@ -148,7 +150,7 @@ export class FeatureOverrides implements WebGLDisposable {
const modelIdParts = Id64.getUint32Pair(map.modelId);
const isModelHilited = allowHilite && hilites.models.has(modelIdParts.lower, modelIdParts.upper);

this._anyOpaque = this._anyTranslucent = this._anyHilited = false;
this._anyOpaque = this._anyTranslucent = this._anyViewIndependentTranslucent = this._anyHilited = false;

let nHidden = 0;
let nOverridden = 0;
Expand Down Expand Up @@ -213,10 +215,15 @@ export class FeatureOverrides implements WebGLDisposable {
alpha = 0xff;

data.setByteAtIndex(dataIndex + 7, alpha);
if (0xff === alpha)
if (0xff === alpha) {
this._anyOpaque = true;
else
} else {
this._anyTranslucent = true;
if (!app.viewDependentTransparency) {
flags |= OvrFlags.ViewIndependentTransparency;
this._anyViewIndependentTranslucent = true;
}
}
}

if (app.overridesWeight && app.weight) {
Expand Down
2 changes: 1 addition & 1 deletion core/frontend/src/render/webgl/RenderCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ export class RenderCommands implements Iterable<DrawCommands> {
this._batchState.push(batch, true);

this.pushAndPop(new PushBatchCommand(batch), PopBatchCommand.instance, () => {
if (this.currentViewFlags.transparency) {
if (this.currentViewFlags.transparency || overrides.anyViewIndependentTranslucent) {
this._opaqueOverrides = overrides.anyOpaque;
this._translucentOverrides = overrides.anyTranslucent;

Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/render/webgl/RenderFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export const enum OvrFlags {
Weight = 1 << 7,
Hilited = 1 << 8,
Emphasized = 1 << 9, // rendered with "emphasis" hilite settings (silhouette etc).
ViewIndependentTransparency = 1 << 10,

Rgba = Rgb | Alpha,
}
Expand Down
28 changes: 22 additions & 6 deletions core/frontend/src/render/webgl/glsl/FeatureSymbology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function addOvrFlagConstants(builder: ShaderBuilder): void {
// NB: We treat the 16-bit flags as 2 bytes - so subtract 8 from each of these bit indices.
builder.addBitFlagConstant("kOvrBit_Hilited", 0);
builder.addBitFlagConstant("kOvrBit_Emphasized", 1);
builder.addBitFlagConstant("kOvrBit_ViewIndependentTransparency", 2);
}

const computeLUTFeatureIndex = `g_featureAndMaterialIndex.xyz`;
Expand Down Expand Up @@ -156,14 +157,19 @@ const checkVertexDiscard = `
if (feature_alpha > 0.0)
hasAlpha = feature_alpha <= s_maxAlpha;

int discardFlags = u_transparencyDiscardFlags;
bool discardViewIndependentDuringOpaque = discardFlags >= 4;
if (discardViewIndependentDuringOpaque)
discardFlags = discardFlags - 4;

bool isOpaquePass = (kRenderPass_OpaqueLinear <= u_renderPass && kRenderPass_OpaqueGeneral >= u_renderPass);
bool discardTranslucentDuringOpaquePass = 1 == u_transparencyDiscardFlags || 3 == u_transparencyDiscardFlags;
bool discardTranslucentDuringOpaquePass = 1 == discardFlags || 3 == discardFlags || (feature_viewIndependentTransparency && discardViewIndependentDuringOpaque);
if (isOpaquePass && !discardTranslucentDuringOpaquePass)
return false;

bool isTranslucentPass = kRenderPass_Translucent == u_renderPass;
bool discardOpaqueDuringTranslucentPass = 2 == u_transparencyDiscardFlags || 3 == u_transparencyDiscardFlags;
if (isTranslucentPass &&!discardOpaqueDuringTranslucentPass)
bool discardOpaqueDuringTranslucentPass = 2 == discardFlags || 3 == discardFlags;
if (isTranslucentPass && !discardOpaqueDuringTranslucentPass)
return false;

return (isOpaquePass && hasAlpha) || (isTranslucentPass && !hasAlpha);
Expand All @@ -174,13 +180,19 @@ function addTransparencyDiscardFlags(vert: VertexShaderBuilder) {
// is used when applying transparency threshold. However, we need to ensure we don't DISCARD transparent stuff during
// opaque pass if transparency is off (see checkVertexDiscard). Especially important for transparency threshold and readPixels().
// Also, if we override raster text to be opaque we must still draw it in the translucent pass.
// Finally, if the transparency override is view-independent (i.e., ignores view flags and render mode) we want to discard it during opaque pass
// unless we're reading pixels.
// So we have a bit field:
// 1: discard translucent during opaque.
// 2: discard opaque during translucent.
// 3: both
// 4: discard view-independent translucent during opaque.
vert.addUniform("u_transparencyDiscardFlags", VariableType.Int, (prog) => {
prog.addGraphicUniform("u_transparencyDiscardFlags", (uniform, params) => {
// During readPixels() we force transparency off. Make sure to ignore a Branch that turns it back on.
let flags = params.target.currentViewFlags.transparency && !params.target.isReadPixelsInProgress ? 1 : 0;
let flags = 0;
if (!params.target.isReadPixelsInProgress)
flags = params.target.currentViewFlags.transparency ? 1 : 4;

if (!params.geometry.alwaysRenderTranslucent)
flags += 2;

Expand Down Expand Up @@ -634,9 +646,11 @@ const computeFeatureOverrides = `
if (rgbOverridden)
feature_rgb = rgba.rgb;

if (alphaOverridden)
if (alphaOverridden) {
feature_alpha = rgba.a;
feature_viewIndependentTransparency = nthFeatureBitSet(emphFlags, kOvrBit_ViewIndependentTransparency);
}
}

linear_feature_overrides = vec4(nthFeatureBitSet(flags, kOvrBit_Weight),
value.w * 256.0,
Expand Down Expand Up @@ -741,6 +755,8 @@ export function addFeatureSymbology(builder: ProgramBuilder, feat: FeatureMode,

const vert = builder.vert;
vert.addGlobal("feature_invisible", VariableType.Boolean, "false");
vert.addGlobal("feature_viewIndependentTransparency", VariableType.Boolean, "false");

addEmphasisFlags(vert);
vert.addGlobal("use_material", VariableType.Boolean, "true");
vert.set(VertexShaderComponent.ComputeFeatureOverrides, computeFeatureOverrides);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ export class Settings implements IDisposable {
handler: (cb) => this.updateAppearance("emphasized", cb.checked ? true : undefined),
});

createCheckBox({
parent: this._element,
name: "View-dependent transparency",
id: "ovr_viewDep",
handler: (cb) => this.updateAppearance("viewDependentTransparency", cb.checked ? true : undefined),
});

const buttonDiv = document.createElement("div");
buttonDiv.style.textAlign = "center";
createButton({
Expand Down Expand Up @@ -175,7 +182,7 @@ export class Settings implements IDisposable {

// private reset() { this._appearance = FeatureSymbology.Appearance.defaults; }

private updateAppearance(field: "rgb" | "transparency" | "linePixels" | "weight" | "ignoresMaterial" | "nonLocatable" | "emphasized", value: any): void {
private updateAppearance(field: "rgb" | "transparency" | "linePixels" | "weight" | "ignoresMaterial" | "nonLocatable" | "emphasized" | "viewDependentTransparency", value: any): void {
const props = this._appearance.toJSON();
props[field] = value;
this._appearance = FeatureAppearance.fromJSON(props);
Expand Down