diff --git a/src/extras/renderers/outline-renderer.js b/src/extras/renderers/outline-renderer.js index 41b0cb26bed..9a0a60b3694 100644 --- a/src/extras/renderers/outline-renderer.js +++ b/src/extras/renderers/outline-renderer.js @@ -4,7 +4,7 @@ import { BlendState } from '../../platform/graphics/blend-state.js'; import { ADDRESS_CLAMP_TO_EDGE, BLENDEQUATION_ADD, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDMODE_SRC_ALPHA, CULLFACE_NONE, - FILTER_LINEAR, FILTER_LINEAR_MIPMAP_LINEAR, PIXELFORMAT_SRGBA8, + FILTER_LINEAR, FILTER_LINEAR_MIPMAP_LINEAR, FRONTFACE_CCW, PIXELFORMAT_SRGBA8, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; @@ -316,6 +316,7 @@ class OutlineRenderer { device.setDepthState(DepthState.NODEPTH); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); device.setBlendState(this.blendState); this.quadRenderer.render(); } diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js index c5808cbcca1..91bf9eab598 100644 --- a/src/platform/graphics/constants.js +++ b/src/platform/graphics/constants.js @@ -344,6 +344,24 @@ export const CULLFACE_FRONT = 2; */ export const CULLFACE_FRONTANDBACK = 3; +/** + * The counter-clock-wise winding. Specifies whether polygons are front- or back-facing by setting a winding orientation. + * + * The counter-clock-wise winding. Specifies whether polygons are front- or back-facing by setting a winding orientation. + * + * @category Graphics + */ +export const FRONTFACE_CCW = 0; + +/** + * The clock-wise winding. Specifies whether polygons are front- or back-facing by setting a winding orientation. + * + * @category Graphics + * + * @category Graphics + */ +export const FRONTFACE_CW = 1; + /** * Point sample filtering. * diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js index 283dc5d5727..14409d8ee9c 100644 --- a/src/platform/graphics/graphics-device.js +++ b/src/platform/graphics/graphics-device.js @@ -14,7 +14,8 @@ import { INDEXFORMAT_UINT16, PRIMITIVE_POINTS, PRIMITIVE_TRIFAN, SEMANTIC_POSITION, TYPE_FLOAT32, PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, DISPLAYFORMAT_LDR, - semanticToLocation + semanticToLocation, + FRONTFACE_CCW } from './constants.js'; import { BlendState } from './blend-state.js'; import { DepthState } from './depth-state.js'; @@ -754,6 +755,7 @@ class GraphicsDevice extends EventHandler { this.blendState = new BlendState(); this.depthState = new DepthState(); this.cullMode = CULLFACE_BACK; + this.frontFace = FRONTFACE_CCW; // Cached viewport and scissor dimensions this.vx = this.vy = this.vw = this.vh = 0; @@ -821,6 +823,19 @@ class GraphicsDevice extends EventHandler { Debug.assert(false); } + /** + * Controls whether polygons are front- or back-facing by setting a winding + * orientation. The default frontFace is {@link FRONTFACE_CCW}. + * + * @param {number} frontFace - The front face to set. Can be: + * + * - {@link FRONTFACE_CW} + * - {@link FRONTFACE_CCW} + */ + setFrontFace(frontFace) { + Debug.assert(false); + } + /** * Sets the specified render target on the device. If null is passed as a parameter, the back * buffer becomes the current target for all rendering operations. diff --git a/src/platform/graphics/null/null-graphics-device.js b/src/platform/graphics/null/null-graphics-device.js index dd935dde275..34fff37589b 100644 --- a/src/platform/graphics/null/null-graphics-device.js +++ b/src/platform/graphics/null/null-graphics-device.js @@ -126,6 +126,9 @@ class NullGraphicsDevice extends GraphicsDevice { setCullMode(cullMode) { } + setFrontFace(frontFace) { + } + setAlphaToCoverage(state) { } diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js index aa5fc4f17fa..ce3fe4bdb7a 100644 --- a/src/platform/graphics/webgl/webgl-graphics-device.js +++ b/src/platform/graphics/webgl/webgl-graphics-device.js @@ -335,6 +335,11 @@ class WebglGraphicsDevice extends GraphicsDevice { gl.FRONT_AND_BACK ]; + this.glFrontFace = [ + gl.CCW, + gl.CW + ]; + this.glFilter = [ gl.NEAREST, gl.LINEAR, @@ -2511,6 +2516,14 @@ class WebglGraphicsDevice extends GraphicsDevice { } } + setFrontFace(frontFace) { + if (this.frontFace !== frontFace) { + const mode = this.glFrontFace[frontFace]; + this.gl.frontFace(mode); + this.frontFace = frontFace; + } + } + /** * Sets the active shader to be used during subsequent draw calls. * diff --git a/src/platform/graphics/webgpu/webgpu-clear-renderer.js b/src/platform/graphics/webgpu/webgpu-clear-renderer.js index 88fcbacd8d9..1c5d970b240 100644 --- a/src/platform/graphics/webgpu/webgpu-clear-renderer.js +++ b/src/platform/graphics/webgpu/webgpu-clear-renderer.js @@ -5,7 +5,8 @@ import { CULLFACE_NONE, PRIMITIVE_TRISTRIP, SHADERLANGUAGE_WGSL, UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC4, BINDGROUP_MESH, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, - BINDGROUP_MESH_UB + BINDGROUP_MESH_UB, + FRONTFACE_CCW } from '../constants.js'; import { Shader } from '../shader.js'; import { DynamicBindGroup } from '../bind-group.js'; @@ -138,6 +139,7 @@ class WebgpuClearRenderer { uniformBuffer.endUpdate(); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); // render 4 vertices without vertex buffer device.setShader(this.shader); diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js index e05a6283289..64d2351946e 100644 --- a/src/platform/graphics/webgpu/webgpu-graphics-device.js +++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js @@ -731,7 +731,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice { // render pipeline pipeline = this.renderPipeline.get(primitive, vb0?.format, vb1?.format, indexBuffer?.format, this.shader, this.renderTarget, this.bindGroupFormats, this.blendState, this.depthState, this.cullMode, - this.stencilEnabled, this.stencilFront, this.stencilBack); + this.stencilEnabled, this.stencilFront, this.stencilBack, this.frontFace); Debug.assert(pipeline); if (this.pipeline !== pipeline) { @@ -850,6 +850,10 @@ class WebgpuGraphicsDevice extends GraphicsDevice { this.cullMode = cullMode; } + setFrontFace(frontFace) { + this.frontFace = frontFace; + } + setAlphaToCoverage(state) { } diff --git a/src/platform/graphics/webgpu/webgpu-render-pipeline.js b/src/platform/graphics/webgpu/webgpu-render-pipeline.js index 9520faa7289..6fe98e67f4d 100644 --- a/src/platform/graphics/webgpu/webgpu-render-pipeline.js +++ b/src/platform/graphics/webgpu/webgpu-render-pipeline.js @@ -72,6 +72,11 @@ const _cullModes = [ 'front' // CULLFACE_FRONT ]; +const _frontFace = [ + 'ccw', // FRONTFACE_CCW + 'cw' // FRONTFACE_CW +]; + const _stencilOps = [ 'keep', // STENCILOP_KEEP 'zero', // STENCILOP_ZERO @@ -107,7 +112,7 @@ class CacheEntry { } class WebgpuRenderPipeline extends WebgpuPipeline { - lookupHashes = new Uint32Array(14); + lookupHashes = new Uint32Array(15); constructor(device) { super(device); @@ -141,11 +146,12 @@ class WebgpuRenderPipeline extends WebgpuPipeline { * @param {boolean} stencilEnabled - Whether stencil is enabled. * @param {StencilParameters} stencilFront - The stencil state for front faces. * @param {StencilParameters} stencilBack - The stencil state for back faces. + * @param {number} frontFace - The front face. * @returns {GPURenderPipeline} Returns the render pipeline. * @private */ get(primitive, vertexFormat0, vertexFormat1, ibFormat, shader, renderTarget, bindGroupFormats, blendState, - depthState, cullMode, stencilEnabled, stencilFront, stencilBack) { + depthState, cullMode, stencilEnabled, stencilFront, stencilBack, frontFace) { Debug.assert(bindGroupFormats.length <= 3); @@ -177,6 +183,7 @@ class WebgpuRenderPipeline extends WebgpuPipeline { lookupHashes[11] = stencilEnabled ? stencilFront.key : 0; lookupHashes[12] = stencilEnabled ? stencilBack.key : 0; lookupHashes[13] = ibFormat ?? 0; + lookupHashes[14] = frontFace; const hash = hash32Fnv1a(lookupHashes); // cached pipeline @@ -206,7 +213,7 @@ class WebgpuRenderPipeline extends WebgpuPipeline { const cacheEntry = new CacheEntry(); cacheEntry.hashes = new Uint32Array(lookupHashes); cacheEntry.pipeline = this.create(primitiveTopology, ibFormat, shader, renderTarget, pipelineLayout, blendState, - depthState, vertexBufferLayout, cullMode, stencilEnabled, stencilFront, stencilBack); + depthState, vertexBufferLayout, cullMode, stencilEnabled, stencilFront, stencilBack, frontFace); // add to cache if (cacheEntries) { @@ -313,7 +320,7 @@ class WebgpuRenderPipeline extends WebgpuPipeline { } create(primitiveTopology, ibFormat, shader, renderTarget, pipelineLayout, blendState, depthState, vertexBufferLayout, - cullMode, stencilEnabled, stencilFront, stencilBack) { + cullMode, stencilEnabled, stencilFront, stencilBack, frontFace) { const wgpu = this.device.wgpu; @@ -330,7 +337,7 @@ class WebgpuRenderPipeline extends WebgpuPipeline { primitive: { topology: primitiveTopology, - frontFace: 'ccw', + frontFace: _frontFace[frontFace], cullMode: _cullModes[cullMode] }, diff --git a/src/scene/graphics/quad-render.js b/src/scene/graphics/quad-render.js index 192ace60893..f583602f265 100644 --- a/src/scene/graphics/quad-render.js +++ b/src/scene/graphics/quad-render.js @@ -29,6 +29,7 @@ const _dynamicBindGroup = new DynamicBindGroup(); * you should set up the following states as needed, otherwise previously set states will be used: * - Blend state via {@link GraphicsDevice#setBlendState} * - Cull mode via {@link GraphicsDevice#setCullMode} + * - FrontFace via {@link GraphicsDevice#setFrontFace} * - Depth state via {@link GraphicsDevice#setDepthState} * - Stencil state via {@link GraphicsDevice#setStencilState} * @@ -46,6 +47,7 @@ const _dynamicBindGroup = new DynamicBindGroup(); * // Set up render states before rendering * app.graphicsDevice.setBlendState(BlendState.NOBLEND); * app.graphicsDevice.setCullMode(CULLFACE_NONE); + * app.graphicsDevice.setFrontFace(FRONTFACE_CCW); * app.graphicsDevice.setDepthState(DepthState.NODEPTH); * app.graphicsDevice.setStencilState(null, null); * diff --git a/src/scene/graphics/render-pass-quad.js b/src/scene/graphics/render-pass-quad.js index a2e4899becb..13ace57045e 100644 --- a/src/scene/graphics/render-pass-quad.js +++ b/src/scene/graphics/render-pass-quad.js @@ -1,4 +1,4 @@ -import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, FRONTFACE_CCW } from '../../platform/graphics/constants.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; import { RenderPass } from '../../platform/graphics/render-pass.js'; @@ -20,6 +20,7 @@ class RenderPassQuad extends RenderPass { DebugGraphics.pushGpuMarker(device, `${this.name}:${this.quad.shader.name}`); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); device.setDepthState(DepthState.NODEPTH); device.setStencilState(null, null); diff --git a/src/scene/graphics/render-pass-shader-quad.js b/src/scene/graphics/render-pass-shader-quad.js index 3ffc45bee0c..2ed175c9b10 100644 --- a/src/scene/graphics/render-pass-shader-quad.js +++ b/src/scene/graphics/render-pass-shader-quad.js @@ -1,6 +1,6 @@ import { QuadRender } from './quad-render.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; -import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, FRONTFACE_CCW } from '../../platform/graphics/constants.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; import { RenderPass } from '../../platform/graphics/render-pass.js'; @@ -33,6 +33,11 @@ class RenderPassShaderQuad extends RenderPass { */ cullMode = CULLFACE_NONE; + /** + * The front face to use when rendering the quad. Defaults to {@link FRONTFACE_CCW}. + */ + frontFace = FRONTFACE_CCW; + /** * A blend state to use when rendering the quad. Defaults to {@link BlendState.NOBLEND}. * @@ -106,6 +111,7 @@ class RenderPassShaderQuad extends RenderPass { const device = this.device; device.setBlendState(this.blendState); device.setCullMode(this.cullMode); + device.setFrontFace(this.frontFace); device.setDepthState(this.depthState); device.setStencilState(this.stencilFront, this.stencilBack); diff --git a/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js b/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js index 20d6f9fd7e9..ad7881fe0be 100644 --- a/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js +++ b/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js @@ -6,7 +6,7 @@ import { RenderPass } from '../../platform/graphics/render-pass.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; -import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, FRONTFACE_CCW } from '../../platform/graphics/constants.js'; /** * @import { GSplatInfo } from './gsplat-info.js' @@ -105,6 +105,7 @@ class GSplatWorkBufferRenderPass extends RenderPass { // Set up render state device.setBlendState(BlendState.NOBLEND); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); device.setDepthState(DepthState.NODEPTH); device.setStencilState(); diff --git a/src/scene/gsplat/gsplat-resolve-sh.js b/src/scene/gsplat/gsplat-resolve-sh.js index a3d9978cb8f..16932fcbc53 100644 --- a/src/scene/gsplat/gsplat-resolve-sh.js +++ b/src/scene/gsplat/gsplat-resolve-sh.js @@ -3,6 +3,7 @@ import { Mat4 } from '../../core/math/mat4.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; import { CULLFACE_NONE, + FRONTFACE_CCW, PIXELFORMAT_RGBA8, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; @@ -290,6 +291,7 @@ class GSplatResolveSH { }); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); device.setDepthState(DepthState.NODEPTH); device.setStencilState(null, null); device.setBlendState(BlendState.NOBLEND); diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index 58b06dc7d68..20130a2b10c 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -6,7 +6,8 @@ import { BLENDEQUATION_ADD, BLENDEQUATION_REVERSE_SUBTRACT, BLENDEQUATION_MIN, BLENDEQUATION_MAX, CULLFACE_BACK, - SHADERLANGUAGE_GLSL + SHADERLANGUAGE_GLSL, + FRONTFACE_CCW } from '../../platform/graphics/constants.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; @@ -167,6 +168,19 @@ class Material { */ cull = CULLFACE_BACK; + /** + * Controls whether polygons are front- or back-facing by setting a winding + * orientation. Can be: + * + * - {@link FRONTFACE_CW}: The clock-wise winding. + * - {@link FRONTFACE_CCW}: The counter-clock-wise winding. + * + * Defaults to {@link FRONTFACE_CCW}. + * + * @type {number} + */ + frontFace = FRONTFACE_CCW; + /** * Stencil parameters for front faces (default is null). * @@ -637,6 +651,7 @@ class Material { this._depthState.copy(source._depthState); this.cull = source.cull; + this.frontFace = source.frontFace; this.stencilFront = source.stencilFront?.clone(); if (source.stencilBack) { diff --git a/src/scene/particle-system/gpu-updater.js b/src/scene/particle-system/gpu-updater.js index 13c031d1282..ba1f24e5c37 100644 --- a/src/scene/particle-system/gpu-updater.js +++ b/src/scene/particle-system/gpu-updater.js @@ -3,7 +3,7 @@ import { Mat3 } from '../../core/math/mat3.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; -import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, FRONTFACE_CCW } from '../../platform/graphics/constants.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; @@ -96,6 +96,7 @@ class ParticleGPUUpdater { device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); this.randomize(); diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index fdc96b28a24..b04c7fd4f4f 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -626,6 +626,7 @@ class ForwardRenderer extends Renderer { DebugGraphics.pushGpuMarker(device, `Node: ${drawCall.node.name}, Material: ${material.name}`); this.setupCullMode(camera._cullFaces, flipFactor, drawCall); + this.setupFrontFace(drawCall); const stencilFront = drawCall.stencilFront ?? material.stencilFront; const stencilBack = drawCall.stencilBack ?? material.stencilBack; diff --git a/src/scene/renderer/render-pass-cookie-renderer.js b/src/scene/renderer/render-pass-cookie-renderer.js index d72b4c60100..87804053181 100644 --- a/src/scene/renderer/render-pass-cookie-renderer.js +++ b/src/scene/renderer/render-pass-cookie-renderer.js @@ -1,7 +1,7 @@ import { Debug } from '../../core/debug.js'; import { Vec4 } from '../../core/math/vec4.js'; import { Mat4 } from '../../core/math/mat4.js'; -import { CULLFACE_NONE, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, FRONTFACE_CCW, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI } from '../constants.js'; import { ShaderUtils } from '../shader-lib/shader-utils.js'; @@ -165,6 +165,7 @@ class RenderPassCookieRenderer extends RenderPass { const device = this.device; device.setBlendState(BlendState.NOBLEND); device.setCullMode(CULLFACE_NONE); + device.setFrontFace(FRONTFACE_CCW); device.setDepthState(DepthState.NODEPTH); device.setStencilState(); diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index eb05b7d3272..7cd3a388832 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -505,6 +505,11 @@ class Renderer { } } + setupFrontFace(drawCall) { + const material = drawCall.material; + this.device.setFrontFace(material.frontFace); + } + updateCameraFrustum(camera) { if (camera.xr && camera.xr.views.list.length) { @@ -552,6 +557,9 @@ class Renderer { // Cull mode device.setCullMode(material.cull); + // Front face + device.setFrontFace(material.frontFace); + // Alpha test if (material.opacityMap) { this.opacityMapId.setValue(material.opacityMap); diff --git a/src/scene/renderer/shadow-renderer.js b/src/scene/renderer/shadow-renderer.js index e3b196fcd03..36e2bd3e80d 100644 --- a/src/scene/renderer/shadow-renderer.js +++ b/src/scene/renderer/shadow-renderer.js @@ -330,6 +330,7 @@ class ShadowRenderer { } renderer.setupCullMode(true, flipFactor, meshInstance); + renderer.setupFrontFace(meshInstance); // Uniforms I (shadow): material material.setParameters(device);