Skip to content

Commit dce2948

Browse files
committed
fix: transparent canvas
1 parent 230c342 commit dce2948

5 files changed

Lines changed: 101 additions & 6 deletions

File tree

src/assets/shader/ShaderLib.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { Clearcoat_frag } from './materials/program/Clearcoat_frag';
3737
import { Lit_shader } from './materials/Lit_shader';
3838
import { PBRLItShader } from './materials/PBRLItShader';
3939
import { BxdfDebug_frag } from './materials/program/BxdfDebug_frag';
40-
import { Quad_depth2d_frag_wgsl, Quad_depthCube_frag_wgsl, Quad_frag_wgsl, Quad_vert_wgsl } from './quad/Quad_shader';
40+
import { Quad_depth2d_frag_wgsl, Quad_depthCube_frag_wgsl, Quad_frag_wgsl, Quad_opaque_frag_wgsl, Quad_premultiply_frag_wgsl, Quad_vert_wgsl } from './quad/Quad_shader';
4141
import { ColorUtil } from './utils/ColorUtil';
4242
import { GenerayRandomDir } from './utils/GenerayRandomDir';
4343
import { MatrixShader } from './math/MatrixShader';
@@ -136,6 +136,8 @@ export class ShaderLib {
136136
ShaderLib.register('GenerayRandomDir', GenerayRandomDir);
137137
ShaderLib.register('Quad_vert_wgsl', Quad_vert_wgsl);
138138
ShaderLib.register('Quad_frag_wgsl', Quad_frag_wgsl);
139+
ShaderLib.register('Quad_premultiply_frag_wgsl', Quad_premultiply_frag_wgsl);
140+
ShaderLib.register('Quad_opaque_frag_wgsl', Quad_opaque_frag_wgsl);
139141
ShaderLib.register('Quad_depth2d_frag_wgsl', Quad_depth2d_frag_wgsl);
140142
ShaderLib.register('Quad_depthCube_frag_wgsl', Quad_depthCube_frag_wgsl);
141143
ShaderLib.register('sky_vs_frag_wgsl', CubeSky_Shader.sky_vs_frag_wgsl);

src/assets/shader/quad/Quad_shader.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,69 @@ export let Quad_frag_wgsl: string = /*wgsl*/ `
8888
return FragmentOutput(color);
8989
}
9090
`
91+
/**
92+
* Present blit for opaque canvases (CanvasConfig.alpha unset/false).
93+
* The swapchain always runs with alphaMode 'premultiplied' (see
94+
* Context3D.init), so whatever alpha the scene left in the color
95+
* buffer would be composited against the page. A default-configured
96+
* canvas must be opaque: a scene with no sky and no opaque geometry
97+
* (e.g. pure additive-particle demos) leaves alpha 0 in the color
98+
* buffer, and without this clamp the page background bleeds through
99+
* the render — the result then changes with the page's CSS color and
100+
* with per-compositor handling of out-of-range premultiplied values.
101+
* @internal
102+
*/
103+
export let Quad_opaque_frag_wgsl: string = /*wgsl*/ `
104+
struct FragmentOutput {
105+
@location(auto) o_Target: vec4<f32>
106+
};
107+
108+
@group(1) @binding(0)
109+
var baseMapSampler: sampler;
110+
@group(1) @binding(1)
111+
var baseMap: texture_2d<f32>;
112+
113+
@fragment
114+
fn main(@location(auto) fragUV: vec2<f32>) -> FragmentOutput {
115+
var uv = fragUV ;
116+
uv.y = 1.0 - uv.y ;
117+
var color: vec4<f32> = textureSample(baseMap, baseMapSampler, uv );
118+
color.a = 1.0;
119+
return FragmentOutput(color);
120+
}
121+
`
122+
/**
123+
* Present blit for transparent canvases (CanvasConfig.alpha).
124+
* The swapchain is configured with alphaMode 'premultiplied', which
125+
* requires every component of rgb <= a; values outside that range are
126+
* undefined per the WebGPU spec (some compositors clamp rgb to a,
127+
* silently erasing additive content whose pixels carry light in rgb
128+
* but 0 coverage in alpha). Lift alpha to the brightest color
129+
* component so the output is always spec-valid: additive light then
130+
* composites as glow that partially occludes the page instead of
131+
* being clamped away.
132+
* @internal
133+
*/
134+
export let Quad_premultiply_frag_wgsl: string = /*wgsl*/ `
135+
struct FragmentOutput {
136+
@location(auto) o_Target: vec4<f32>
137+
};
138+
139+
@group(1) @binding(0)
140+
var baseMapSampler: sampler;
141+
@group(1) @binding(1)
142+
var baseMap: texture_2d<f32>;
143+
144+
@fragment
145+
fn main(@location(auto) fragUV: vec2<f32>) -> FragmentOutput {
146+
var uv = fragUV ;
147+
uv.y = 1.0 - uv.y ;
148+
var color: vec4<f32> = textureSample(baseMap, baseMapSampler, uv );
149+
let coverage = saturate(max(color.r, max(color.g, color.b)));
150+
color.a = max(color.a, coverage);
151+
return FragmentOutput(color);
152+
}
153+
`
91154
/**
92155
* @internal
93156
*/

src/gfx/graphics/webGpu/shader/RenderShaderPass.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1119,9 +1119,18 @@ export class RenderShaderPass extends ShaderPassBase {
11191119
// - **Early-Z is still active for opaques.** The depth
11201120
// test runs before the fragment shader; overdrawn
11211121
// opaques are rejected before the lit shader runs.
1122+
// - Only the default `less`/`less_equal` compares get
1123+
// the 1-ULP relaxation. A material that explicitly
1124+
// sets another compare (`always`, `greater`, …) —
1125+
// e.g. Graphic3D overlays toggling depth-test off —
1126+
// must keep it; forcing `less_equal` here silently
1127+
// disabled `material.depthCompare` for every
1128+
// zPrePass-on scene.
11221129
renderPipelineDescriptor[`depthStencil`] = {
11231130
depthWriteEnabled: shaderState.depthWriteEnabled,
1124-
depthCompare: GPUCompareFunction.less_equal,
1131+
depthCompare: shaderState.depthCompare == GPUCompareFunction.less
1132+
? GPUCompareFunction.less_equal
1133+
: shaderState.depthCompare,
11251134
format: renderPassState.zPreTexture.format,
11261135
};
11271136
} else {

src/gfx/renderJob/graph/passes/_present.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,22 @@ import { RTFrame } from '../../frame/RTFrame';
1313
* `Quad_frag_wgsl` — the present blit shaders registered in
1414
* `ShaderLib` at engine init. The returned quad has no RT
1515
* attachments because the swapchain view is acquired per-frame from
16-
* `Context3D.context.getCurrentTexture()`. */
16+
* `Context3D.context.getCurrentTexture()`.
17+
*
18+
* The swapchain always runs in premultiplied alpha mode, so the blit
19+
* must leave it with well-defined alpha:
20+
* - Transparent canvases (CanvasConfig.alpha) blit through
21+
* `Quad_premultiply_frag_wgsl`, which lifts alpha to the brightest
22+
* color component so the swapchain never carries rgb > alpha —
23+
* out-of-range premultiplied values are undefined and real
24+
* compositors clamp them, wiping additive-blended content.
25+
* - Opaque canvases (the default) blit through
26+
* `Quad_opaque_frag_wgsl`, which forces alpha to 1 so the page
27+
* background never bleeds through scenes that leave alpha 0 in
28+
* the color buffer (no sky + only blended geometry). */
1729
export function createPresentQuad(ctx: Context3D): ViewQuad {
18-
return new ViewQuad(ctx, `Quad_vert_wgsl`, `Quad_frag_wgsl`, new RTFrame([], []), 0, false);
30+
const frag = ctx.canvasConfig?.alpha ? `Quad_premultiply_frag_wgsl` : `Quad_opaque_frag_wgsl`;
31+
return new ViewQuad(ctx, `Quad_vert_wgsl`, frag, new RTFrame([], []), 0, false);
1932
}
2033

2134
/** Copy `texture` to the swapchain via the present quad. Issues its

src/materials/BlendMode.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ export class BlendFactor {
9696
blend.color.dstFactor = 'one';
9797
blend.color.operation = `add`;
9898

99-
blend.alpha.srcFactor = `one`;
99+
// Additive blending contributes light, not coverage —
100+
// keep the destination alpha untouched. Accumulating
101+
// src alpha (`one`+`one`) turned every covered pixel
102+
// opaque, so on a transparent canvas
103+
// (CanvasConfig.alpha) additive sprites with opaque
104+
// black textures composited as solid black blocks
105+
// instead of glowing over the page background.
106+
blend.alpha.srcFactor = `zero`;
100107
blend.alpha.dstFactor = `one`;
101108
blend.alpha.operation = `add`;
102109
break;
@@ -130,7 +137,8 @@ export class BlendFactor {
130137
blend.color.dstFactor = 'one';
131138
blend.color.operation = `max`;
132139

133-
blend.alpha.srcFactor = `one`;
140+
// Same coverage rule as ADD above.
141+
blend.alpha.srcFactor = `zero`;
134142
blend.alpha.dstFactor = `one`;
135143
blend.alpha.operation = `add`;
136144
break;

0 commit comments

Comments
 (0)