diff --git a/.changeset/witty-turtles-design.md b/.changeset/witty-turtles-design.md new file mode 100644 index 000000000..f38f6fe06 --- /dev/null +++ b/.changeset/witty-turtles-design.md @@ -0,0 +1,7 @@ +--- +'@antv/g-plugin-device-renderer': patch +'@antv/g-webgl': patch +'@antv/g-lite': patch +--- + +Pass webxr frame on each tick when rendering. diff --git a/__tests__/demos/3d/index.ts b/__tests__/demos/3d/index.ts index 245f6459b..114e25d29 100644 --- a/__tests__/demos/3d/index.ts +++ b/__tests__/demos/3d/index.ts @@ -3,3 +3,4 @@ export { sphere } from './sphere'; export { torus } from './torus'; export { cylinder } from './cylinder'; export { force } from './force'; +export { ar } from './webar'; diff --git a/__tests__/demos/3d/webar.ts b/__tests__/demos/3d/webar.ts new file mode 100644 index 000000000..316fbf733 --- /dev/null +++ b/__tests__/demos/3d/webar.ts @@ -0,0 +1,64 @@ +import { CanvasEvent } from '../../../packages/g'; +import { + MeshBasicMaterial, + CubeGeometry, + Mesh, + Plugin as Plugin3D, +} from '../../../packages/g-plugin-3d'; +import { Plugin as PluginControl } from '../../../packages/g-plugin-control'; +import { ARButton, DeviceRenderer } from '../../../packages/g-webgl'; + +export async function ar(context) { + const { canvas, renderer, container } = context; + + // wait for canvas' initialization complete + await canvas.ready; + + // use GPU device + const plugin = renderer.getPlugin('device-renderer') as DeviceRenderer.Plugin; + const device = plugin.getDevice(); + + // 1. load texture with URL + const map = plugin.loadTexture( + 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*_aqoS73Se3sAAAAAAAAAAAAAARQnAQ', + ); + + const cubeGeometry = new CubeGeometry(device, { + width: 200, + height: 200, + depth: 200, + }); + const basicMaterial = new MeshBasicMaterial(device, { + // wireframe: true, + map, + }); + + const cube = new Mesh({ + style: { + fill: '#1890FF', + opacity: 1, + geometry: cubeGeometry, + material: basicMaterial, + }, + }); + + cube.setPosition(300, 250, 200); + + canvas.appendChild(cube); + + canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + cube.rotate(1, 0, 0); + }); + + canvas.getConfig().disableHitTesting = true; + + const $button = ARButton.createButton(canvas, renderer, {}); + container.appendChild($button); +} + +ar.initRenderer = (renderer, type) => { + if (type === 'webgl' || type === 'webgpu') { + renderer.registerPlugin(new Plugin3D()); + renderer.registerPlugin(new PluginControl()); + } +}; diff --git a/__tests__/main.ts b/__tests__/main.ts index 3babf869e..c46aebfd6 100644 --- a/__tests__/main.ts +++ b/__tests__/main.ts @@ -189,6 +189,7 @@ function createSpecRender(object) { ], // Used for WebGPU renderer shaderCompilerPath: '/glsl_wgsl_compiler_bg.wasm', + // enableAutoRendering: false, // enableDirtyRectangleRendering: false, // enableDirtyRectangleRenderingDebug: true, }); diff --git a/packages/g-lite/package.json b/packages/g-lite/package.json index 962260aef..fdc6fc44f 100644 --- a/packages/g-lite/package.json +++ b/packages/g-lite/package.json @@ -53,7 +53,8 @@ "@types/d3-color": "^3.0.2", "@types/gl-matrix": "^2.4.5", "@types/offscreencanvas": "^2019.6.4", - "@types/rbush": "^3.0.0" + "@types/rbush": "^3.0.0", + "@types/webxr": "0.5.5" }, "publishConfig": { "access": "public" diff --git a/packages/g-lite/src/Canvas.ts b/packages/g-lite/src/Canvas.ts index 806eb057a..984394587 100644 --- a/packages/g-lite/src/Canvas.ts +++ b/packages/g-lite/src/Canvas.ts @@ -481,11 +481,11 @@ export class Canvas extends EventTarget implements ICanvas { this.document.documentElement.destroyChildren(); } - render() { + render(frame?: XRFrame) { this.dispatchEvent(beforeRenderEvent); const renderingService = this.getRenderingService(); - renderingService.render(this.getConfig(), () => { + renderingService.render(this.getConfig(), frame, () => { // trigger actual rerender event // @see https://github.com/antvis/G/issues/1268 this.dispatchEvent(rerenderEvent); @@ -495,11 +495,11 @@ export class Canvas extends EventTarget implements ICanvas { } private run() { - const tick = () => { - this.render(); + const tick = (time: number, frame?: XRFrame) => { + this.render(frame); this.frameId = this.requestAnimationFrame(tick); }; - tick(); + tick(0); } private initRenderer(renderer: IRenderer, firstContentfullPaint = false) { diff --git a/packages/g-lite/src/camera/Camera.ts b/packages/g-lite/src/camera/Camera.ts index b0955eda2..6fd153013 100644 --- a/packages/g-lite/src/camera/Camera.ts +++ b/packages/g-lite/src/camera/Camera.ts @@ -305,6 +305,13 @@ export class Camera implements ICamera { return this; } + /** + * Set projection matrix manually. + */ + setProjectionMatrix(matrix: mat4) { + this.projectionMatrix = matrix; + } + setFov(fov: number) { this.setPerspective(this.near, this.far, fov, this.aspect); return this; diff --git a/packages/g-lite/src/services/RenderingService.ts b/packages/g-lite/src/services/RenderingService.ts index 12b20a15a..d7113b231 100644 --- a/packages/g-lite/src/services/RenderingService.ts +++ b/packages/g-lite/src/services/RenderingService.ts @@ -84,7 +84,7 @@ export class RenderingService { /** * called at beginning of each frame, won't get called if nothing to re-render */ - beginFrame: new SyncHook<[]>(), + beginFrame: new SyncHook<[XRFrame]>(), /** * called before every dirty object get rendered */ @@ -97,7 +97,7 @@ export class RenderingService { * called after every dirty object get rendered */ afterRender: new SyncHook<[DisplayObject]>(), - endFrame: new SyncHook<[]>(), + endFrame: new SyncHook<[XRFrame]>(), destroy: new SyncHook<[]>(), /** * use async but faster method such as GPU-based picking in `g-plugin-device-renderer` @@ -161,7 +161,11 @@ export class RenderingService { ); } - render(canvasConfig: Partial, rerenderCallback: () => void) { + render( + canvasConfig: Partial, + frame: XRFrame, + rerenderCallback: () => void, + ) { this.stats.total = 0; this.stats.rendered = 0; this.zIndexCounter = 0; @@ -191,7 +195,7 @@ export class RenderingService { ); } - this.hooks.beginFrame.call(); + this.hooks.beginFrame.call(frame); if (shouldTriggerRenderHooks) { renderingContext.renderListCurrentFrame.forEach((object) => { @@ -201,7 +205,7 @@ export class RenderingService { }); } - this.hooks.endFrame.call(); + this.hooks.endFrame.call(frame); renderingContext.renderListCurrentFrame = []; renderingContext.renderReasons.clear(); diff --git a/packages/g-plugin-device-renderer/package.json b/packages/g-plugin-device-renderer/package.json index ae06110d1..4950b08ea 100644 --- a/packages/g-plugin-device-renderer/package.json +++ b/packages/g-plugin-device-renderer/package.json @@ -54,6 +54,7 @@ "@rollup/plugin-wasm": "^5.1.2", "@types/earcut": "^2.1.1", "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "0.5.5", "glslify-import": "3.1.0", "rollup-plugin-glslify": "^1.2.1" }, diff --git a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts index e195b5455..b4b68da3e 100644 --- a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts +++ b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts @@ -8,24 +8,25 @@ import type { RenderingPluginContext, } from '@antv/g-lite'; import { CanvasEvent, ElementEvent, Shape, parseColor } from '@antv/g-lite'; -import { Renderable3D } from './components/Renderable3D'; -import type { LightPool } from './LightPool'; -import { Fog, Light } from './lights'; -import { pushFXAAPass } from './passes/FXAA'; -import { +import type { + Color, Device, SwapChain, Texture, TextureDescriptor, - TransparentBlack, - TransparentWhite, } from '@antv/g-device-api'; import { BlendFactor, BlendMode, colorNewFromRGBA, setAttachmentStateSimple, + TransparentBlack, + TransparentWhite, } from '@antv/g-device-api'; +import { Renderable3D } from './components/Renderable3D'; +import type { LightPool } from './LightPool'; +import { Fog, Light } from './lights'; +import { pushFXAAPass } from './passes/FXAA'; import type { RGGraphBuilder, RenderHelper } from './render'; import { AntialiasingMode, @@ -259,175 +260,240 @@ export class RenderGraphPlugin implements RenderingPlugin { /** * build frame graph at the beginning of each frame */ - renderingService.hooks.beginFrame.tap(RenderGraphPlugin.tag, () => { - const canvas = this.swapChain.getCanvas() as HTMLCanvasElement; - const renderInstManager = this.renderHelper.renderInstManager; - this.builder = this.renderHelper.renderGraph.newGraphBuilder(); - - let clearColor; - if (this.context.config.background === 'transparent') { - clearColor = TransparentBlack; - } else { - // use canvas.background - const backgroundColor = parseColor( - this.context.config.background, - ) as CSSRGB; - - clearColor = this.context.config.background - ? // use premultipliedAlpha - // @see https://canvatechblog.com/alpha-blending-and-webgl-99feb392779e - colorNewFromRGBA( - (Number(backgroundColor.r) / 255) * Number(backgroundColor.alpha), - (Number(backgroundColor.g) / 255) * Number(backgroundColor.alpha), - (Number(backgroundColor.b) / 255) * Number(backgroundColor.alpha), - Number(backgroundColor.alpha), - ) - : TransparentWhite; - } + renderingService.hooks.beginFrame.tap( + RenderGraphPlugin.tag, + (frame: XRFrame) => { + const session = frame?.session; + // const { width, height } = this.context.config; + if (session) { + // const camera = this.context.camera; + // Assumed to be a XRWebGLLayer for now. + let layer = session.renderState.baseLayer; + if (!layer) { + layer = session.renderState.layers![0] as XRWebGLLayer; + } else { + // Bind the graphics framebuffer to the baseLayer's framebuffer. + // Only baseLayer has framebuffer and we need to bind it, even if it is null (for inline sessions). + // gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer); + } - // retrieve at each frame since canvas may resize - const renderInput = { - backbufferWidth: canvas.width, - backbufferHeight: canvas.height, - antialiasingMode: AntialiasingMode.None, - }; - // create main Color RT - const mainRenderDesc = makeBackbufferDescSimple( - RGAttachmentSlot.Color0, - renderInput, - makeAttachmentClearDescriptor(clearColor), - ); - // create main Depth RT - const mainDepthDesc = makeBackbufferDescSimple( - RGAttachmentSlot.DepthStencil, - renderInput, - opaqueWhiteFullClearRenderPassDescriptor, - ); - const mainColorTargetID = this.builder.createRenderTargetID( - mainRenderDesc, - 'Main Color', - ); - const mainDepthTargetID = this.builder.createRenderTargetID( - mainDepthDesc, - 'Main Depth', - ); + this.swapChain.configureSwapChain( + layer.framebufferWidth, + layer.framebufferHeight, + layer.framebuffer, + ); + + // @ts-ignore + const referenceSpace = session.referenceSpace as XRReferenceSpace; + // Retrieve the pose of the device. + // XRFrame.getViewerPose can return null while the session attempts to establish tracking. + const pose = frame.getViewerPose(referenceSpace); + if (pose) { + // const p = pose.transform.position; + // In mobile AR, we only have one view. + // const view = pose.views[0]; + // const viewport = session.renderState.baseLayer!.getViewport(view)!; + // Use the view's transform matrix and projection matrix + // const viewMatrix = mat4.invert(mat4.create(), view.transform.matrix); + // const cameraMatrix = view.transform.matrix; + // const projectionMatrix = view.projectionMatrix; + // @ts-ignore + // camera.setProjectionMatrix(projectionMatrix); + // camera.setViewOffset( + // camera.getView().fullWidth, + // camera.getView().fullHeight, + // 0, + // 0, + // viewport.width, + // viewport.height, + // ); + // camera.setMatrix(cameraMatrix); + // console.log(viewport, camera.getView()); + } + } + + const canvas = this.swapChain.getCanvas() as HTMLCanvasElement; + const renderInstManager = this.renderHelper.renderInstManager; + this.builder = this.renderHelper.renderGraph.newGraphBuilder(); - // main render pass - this.builder.pushPass((pass) => { - pass.setDebugName('Main Render Pass'); - pass.attachRenderTargetID(RGAttachmentSlot.Color0, mainColorTargetID); - pass.attachRenderTargetID( + let clearColor: Color; + if (this.context.config.background === 'transparent') { + clearColor = TransparentBlack; + } else { + // use canvas.background + const backgroundColor = parseColor( + this.context.config.background, + ) as CSSRGB; + + clearColor = this.context.config.background + ? // use premultipliedAlpha + // @see https://canvatechblog.com/alpha-blending-and-webgl-99feb392779e + colorNewFromRGBA( + (Number(backgroundColor.r) / 255) * + Number(backgroundColor.alpha), + (Number(backgroundColor.g) / 255) * + Number(backgroundColor.alpha), + (Number(backgroundColor.b) / 255) * + Number(backgroundColor.alpha), + Number(backgroundColor.alpha), + ) + : TransparentWhite; + } + + // retrieve at each frame since canvas may resize + const renderInput = { + backbufferWidth: canvas.width, + backbufferHeight: canvas.height, + antialiasingMode: AntialiasingMode.None, + }; + // create main Color RT + const mainRenderDesc = makeBackbufferDescSimple( + RGAttachmentSlot.Color0, + renderInput, + makeAttachmentClearDescriptor(clearColor), + ); + // create main Depth RT + const mainDepthDesc = makeBackbufferDescSimple( RGAttachmentSlot.DepthStencil, - mainDepthTargetID, + renderInput, + opaqueWhiteFullClearRenderPassDescriptor, + ); + const mainColorTargetID = this.builder.createRenderTargetID( + mainRenderDesc, + 'Main Color', ); - pass.exec((passRenderer) => { - this.renderLists.world.drawOnPassRenderer( - renderInstManager.renderCache, - passRenderer, + const mainDepthTargetID = this.builder.createRenderTargetID( + mainDepthDesc, + 'Main Depth', + ); + + // main render pass + this.builder.pushPass((pass) => { + pass.setDebugName('Main Render Pass'); + pass.attachRenderTargetID(RGAttachmentSlot.Color0, mainColorTargetID); + pass.attachRenderTargetID( + RGAttachmentSlot.DepthStencil, + mainDepthTargetID, ); + pass.exec((passRenderer) => { + this.renderLists.world.drawOnPassRenderer( + renderInstManager.renderCache, + passRenderer, + ); + }); }); - }); - - // TODO: other post-processing passes - if (this.options?.enableFXAA) { - // FXAA - pushFXAAPass( - this.builder, - this.renderHelper, - renderInput, + + // TODO: other post-processing passes + if (this.options?.enableFXAA) { + // FXAA + pushFXAAPass( + this.builder, + this.renderHelper, + renderInput, + mainColorTargetID, + ); + } + + // output to screen + this.builder.resolveRenderTargetToExternalTexture( mainColorTargetID, + this.swapChain.getOnscreenTexture(), ); - } + }, + ); - // output to screen - this.builder.resolveRenderTargetToExternalTexture( - mainColorTargetID, - this.swapChain.getOnscreenTexture(), - ); - }); + renderingService.hooks.endFrame.tap( + RenderGraphPlugin.tag, + (frame: XRFrame) => { + const renderInstManager = this.renderHelper.renderInstManager; + + // TODO: time for GPU Animation + // const timeInMilliseconds = window.performance.now(); + + // Push our outer template, which contains the dynamic UBO bindings... + const template = this.renderHelper.pushTemplateRenderInst(); + // SceneParams: binding = 0, ObjectParams: binding = 1 + template.setBindingLayout({ numUniformBuffers: 2, numSamplers: 0 }); + template.setMegaStateFlags( + setAttachmentStateSimple( + { + depthWrite: true, + blendConstant: TransparentBlack, + }, + { + rgbBlendMode: BlendMode.ADD, + alphaBlendMode: BlendMode.ADD, + rgbBlendSrcFactor: BlendFactor.SRC_ALPHA, + alphaBlendSrcFactor: BlendFactor.ONE, + rgbBlendDstFactor: BlendFactor.ONE_MINUS_SRC_ALPHA, + alphaBlendDstFactor: BlendFactor.ONE_MINUS_SRC_ALPHA, + }, + ), + ); - renderingService.hooks.endFrame.tap(RenderGraphPlugin.tag, () => { - const renderInstManager = this.renderHelper.renderInstManager; + // Update Scene Params + const { width, height } = this.context.config; + const camera = this.context.camera; - // TODO: time for GPU Animation - // const timeInMilliseconds = window.performance.now(); + // console.log( + // camera.getPerspective(), + // camera.getViewTransform(), + // camera.getPosition(), + // ); - // Push our outer template, which contains the dynamic UBO bindings... - const template = this.renderHelper.pushTemplateRenderInst(); - // SceneParams: binding = 0, ObjectParams: binding = 1 - template.setBindingLayout({ numUniformBuffers: 2, numSamplers: 0 }); - template.setMegaStateFlags( - setAttachmentStateSimple( + template.setUniforms(SceneUniformBufferIndex, [ { - depthWrite: true, - blendConstant: TransparentBlack, + name: SceneUniform.PROJECTION_MATRIX, + value: camera.getPerspective(), }, { - rgbBlendMode: BlendMode.ADD, - alphaBlendMode: BlendMode.ADD, - rgbBlendSrcFactor: BlendFactor.SRC_ALPHA, - alphaBlendSrcFactor: BlendFactor.ONE, - rgbBlendDstFactor: BlendFactor.ONE_MINUS_SRC_ALPHA, - alphaBlendDstFactor: BlendFactor.ONE_MINUS_SRC_ALPHA, + name: SceneUniform.VIEW_MATRIX, + value: camera.getViewTransform(), }, - ), - ); + { + name: SceneUniform.CAMERA_POSITION, + value: camera.getPosition(), + }, + { + name: SceneUniform.DEVICE_PIXEL_RATIO, + value: this.context.contextService.getDPR(), + }, + { + name: SceneUniform.VIEWPORT, + value: [width, height], + }, + { + name: SceneUniform.IS_ORTHO, + value: camera.isOrtho() ? 1 : 0, + }, + { + name: SceneUniform.IS_PICKING, + value: 0, + }, + ]); - // Update Scene Params - const { width, height } = this.context.config; - const camera = this.context.camera; - template.setUniforms(SceneUniformBufferIndex, [ - { - name: SceneUniform.PROJECTION_MATRIX, - value: camera.getPerspective(), - }, - { - name: SceneUniform.VIEW_MATRIX, - value: camera.getViewTransform(), - }, - { - name: SceneUniform.CAMERA_POSITION, - value: camera.getPosition(), - }, - { - name: SceneUniform.DEVICE_PIXEL_RATIO, - value: this.context.contextService.getDPR(), - }, - { - name: SceneUniform.VIEWPORT, - value: [width, height], - }, - { - name: SceneUniform.IS_ORTHO, - value: camera.isOrtho() ? 1 : 0, - }, - { - name: SceneUniform.IS_PICKING, - value: 0, - }, - ]); - - this.batchManager.render(this.renderLists.world); - - renderInstManager.popTemplateRenderInst(); - - this.renderHelper.prepareToRender(); - this.renderHelper.renderGraph.execute(); - - renderInstManager.resetRenderInsts(); - - // capture here since we don't preserve drawing buffer - if (this.enableCapture && this.resolveCapturePromise) { - const { type, encoderOptions } = this.captureOptions; - const dataURL = ( - this.context.contextService.getDomElement() as HTMLCanvasElement - ).toDataURL(type, encoderOptions); - this.resolveCapturePromise(dataURL); - this.enableCapture = false; - this.captureOptions = undefined; - this.resolveCapturePromise = undefined; - } - }); + this.batchManager.render(this.renderLists.world); + + renderInstManager.popTemplateRenderInst(); + + this.renderHelper.prepareToRender(); + this.renderHelper.renderGraph.execute(); + + renderInstManager.resetRenderInsts(); + + // capture here since we don't preserve drawing buffer + if (this.enableCapture && this.resolveCapturePromise) { + const { type, encoderOptions } = this.captureOptions; + const dataURL = ( + this.context.contextService.getDomElement() as HTMLCanvasElement + ).toDataURL(type, encoderOptions); + this.resolveCapturePromise(dataURL); + this.enableCapture = false; + this.captureOptions = undefined; + this.resolveCapturePromise = undefined; + } + }, + ); } /** diff --git a/packages/g-plugin-device-renderer/src/drawcalls/Mesh.ts b/packages/g-plugin-device-renderer/src/drawcalls/Mesh.ts index 473a696fe..60f19c6a4 100644 --- a/packages/g-plugin-device-renderer/src/drawcalls/Mesh.ts +++ b/packages/g-plugin-device-renderer/src/drawcalls/Mesh.ts @@ -3,7 +3,7 @@ import { Shape } from '@antv/g-lite'; import type { Mesh } from '../Mesh'; import { Instanced } from './Instanced'; export class MeshDrawcall extends Instanced { - protected mergeXYZIntoModelMatrix = true; + protected mergeXYZIntoModelMatrix = false; shouldMerge(object: DisplayObject, index: number) { const shouldMerge = super.shouldMerge(object, index); diff --git a/packages/g-plugin-device-renderer/src/index.ts b/packages/g-plugin-device-renderer/src/index.ts index d91dcca4d..d2e2824f5 100644 --- a/packages/g-plugin-device-renderer/src/index.ts +++ b/packages/g-plugin-device-renderer/src/index.ts @@ -114,6 +114,10 @@ export class Plugin extends AbstractRendererPlugin { return this.getRenderGraphPlugin().getDevice(); } + getSwapChain() { + return this.getRenderGraphPlugin().getSwapChain(); + } + loadTexture( src: string | TexImageSource, descriptor?: TextureDescriptor, diff --git a/packages/g-webgl/package.json b/packages/g-webgl/package.json index 7641c5ce8..531e83c47 100644 --- a/packages/g-webgl/package.json +++ b/packages/g-webgl/package.json @@ -49,7 +49,8 @@ }, "devDependencies": { "@types/gl-matrix": "^2.4.5", - "@types/offscreencanvas": "^2019.6.4" + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "0.5.5" }, "publishConfig": { "access": "public" diff --git a/packages/g-webgl/src/ARButton.ts b/packages/g-webgl/src/ARButton.ts new file mode 100644 index 000000000..f550e0124 --- /dev/null +++ b/packages/g-webgl/src/ARButton.ts @@ -0,0 +1,181 @@ +import { Canvas } from '@antv/g-lite'; +import { Renderer } from '.'; + +/** + * @see https://github.com/mrdoob/three.js/blob/master/examples/jsm/webxr/ARButton.js + * @example + * + * import { ARButton } from '@antv/g-webgl'; + * const $button = ARButton.createButton(renderer, { domOverlay: { root: document.body } }); + */ +export class ARButton { + static createButton( + canvas: Canvas, + renderer: Renderer, + sessionInit: XRSessionInit = {}, + ) { + const button = document.createElement('button'); + + const disableButton = () => { + button.style.display = ''; + + button.style.cursor = 'auto'; + button.style.left = 'calc(50% - 75px)'; + button.style.width = '150px'; + + button.onmouseenter = null; + button.onmouseleave = null; + + button.onclick = null; + }; + + const showARNotSupported = () => { + disableButton(); + button.textContent = 'AR NOT SUPPORTED'; + }; + + const showARNotAllowed = (exception: Error) => { + disableButton(); + + console.warn( + 'Exception when trying to call xr.isSessionSupported', + exception, + ); + + button.textContent = 'AR NOT ALLOWED'; + }; + + const stylizeElement = (element: HTMLElement) => { + element.style.position = 'absolute'; + element.style.bottom = '20px'; + element.style.padding = '12px 6px'; + element.style.border = '1px solid #fff'; + element.style.borderRadius = '4px'; + element.style.background = 'rgba(0,0,0,0.1)'; + element.style.color = '#fff'; + element.style.font = 'normal 13px sans-serif'; + element.style.textAlign = 'center'; + element.style.opacity = '0.5'; + element.style.outline = 'none'; + element.style.zIndex = '999'; + }; + + const showStartAR = () => { + let currentSession: XRSession; + button.style.display = ''; + button.style.cursor = 'pointer'; + button.style.left = 'calc(50% - 50px)'; + button.style.width = '100px'; + button.textContent = 'START AR'; + + const onSessionEnded = () => { + currentSession.removeEventListener('end', onSessionEnded); + + button.textContent = 'START AR'; + (sessionInit.domOverlay.root as HTMLElement).style.display = 'none'; + currentSession = undefined; + }; + + const onSessionStarted = async (session: XRSession) => { + session.addEventListener('end', onSessionEnded); + + renderer.xr.setReferenceSpaceType('local'); + + await renderer.xr.setSession(canvas, session); + + button.textContent = 'STOP AR'; + (sessionInit.domOverlay.root as HTMLElement).style.display = ''; + currentSession = session; + }; + + if (sessionInit.domOverlay === undefined) { + const overlay = document.createElement('div'); + overlay.style.display = 'none'; + document.body.appendChild(overlay); + + const svg = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg', + ); + svg.setAttribute('width', '38'); + svg.setAttribute('height', '38'); + svg.style.position = 'absolute'; + svg.style.right = '20px'; + svg.style.top = '20px'; + svg.addEventListener('click', function () { + currentSession.end(); + }); + overlay.appendChild(svg); + + const path = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path', + ); + path.setAttribute('d', 'M 12,12 L 28,28 M 28,12 12,28'); + path.setAttribute('stroke', '#fff'); + path.setAttribute('stroke-width', '2'); + svg.appendChild(path); + + if (sessionInit.optionalFeatures === undefined) { + sessionInit.optionalFeatures = []; + } + + sessionInit.optionalFeatures.push('dom-overlay'); + sessionInit.domOverlay = { root: overlay }; + } + + /** + * Bind event listeners to button. + */ + button.onclick = () => { + if (!currentSession) { + navigator.xr + .requestSession('immersive-ar', sessionInit) + .then(onSessionStarted); + } else { + currentSession.end(); + } + }; + button.onmouseenter = () => { + button.style.opacity = '1.0'; + }; + button.onmouseleave = () => { + button.style.opacity = '0.5'; + }; + }; + + if ('xr' in navigator) { + button.id = 'ARButton'; + button.style.display = 'none'; + + stylizeElement(button); + + navigator.xr + .isSessionSupported('immersive-ar') + .then(function (supported) { + supported ? showStartAR() : showARNotSupported(); + }) + .catch(showARNotAllowed); + + return button; + } else { + const message = document.createElement('a'); + + if (window.isSecureContext === false) { + message.href = document.location.href.replace(/^http:/, 'https:'); + message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message + } else { + message.href = 'https://immersiveweb.dev/'; + message.innerHTML = 'WEBXR NOT AVAILABLE'; + } + + message.style.left = 'calc(50% - 90px)'; + message.style.width = '180px'; + message.style.textDecoration = 'none'; + + stylizeElement(message); + + return message; + } + } +} diff --git a/packages/g-webgl/src/WebXRManager.ts b/packages/g-webgl/src/WebXRManager.ts new file mode 100644 index 000000000..a50ce577d --- /dev/null +++ b/packages/g-webgl/src/WebXRManager.ts @@ -0,0 +1,98 @@ +import { Canvas } from '@antv/g-lite'; +import { DeviceRenderer } from '.'; + +export class WebXRManager { + private session: XRSession; + private referenceSpaceType: XRReferenceSpaceType; + private referenceSpace: XRReferenceSpace; + private glBaseLayer: XRWebGLLayer; + + constructor(private plugin: DeviceRenderer.Plugin) {} + + async setSession(canvas: Canvas, session: XRSession) { + if (session) { + this.session = session; + const gl = this.plugin.getDevice()['gl'] as WebGL2RenderingContext; + // const swapChain = this.plugin.getSwapChain(); + const attributes = gl.getContextAttributes(); + + if (attributes.xrCompatible !== true) { + // @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/makeXRCompatible + await gl.makeXRCompatible(); + } + + // session.addEventListener('select', this.onSessionEvent); + session.addEventListener('end', this.onSessionEnd); + + if (session.renderState.layers === undefined) { + const layerInit = { + antialias: attributes.antialias, + alpha: true, + depth: attributes.depth, + stencil: attributes.stencil, + framebufferScaleFactor: 1.0, + }; + + this.glBaseLayer = new XRWebGLLayer(session, gl, layerInit); + session.updateRenderState({ baseLayer: this.glBaseLayer }); + + this.referenceSpace = await session.requestReferenceSpace( + this.referenceSpaceType, + ); + + // @ts-ignore + session.referenceSpace = this.referenceSpace; + } + + canvas.requestAnimationFrame = + session.requestAnimationFrame.bind(session); + + // const onXRFrame: XRFrameRequestCallback = (time, frame) => { + // // Assumed to be a XRWebGLLayer for now. + // let layer = session.renderState.baseLayer; + // if (!layer) { + // layer = session.renderState.layers![0] as XRWebGLLayer; + // } else { + // // Bind the graphics framebuffer to the baseLayer's framebuffer. + // // Only baseLayer has framebuffer and we need to bind it, even if it is null (for inline sessions). + // // gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer); + // } + + // swapChain.configureSwapChain( + // layer.framebufferWidth, + // layer.framebufferHeight, + // layer.framebuffer, + // ); + + // // Retrieve the pose of the device. + // // XRFrame.getViewerPose can return null while the session attempts to establish tracking. + // const pose = frame.getViewerPose(this.referenceSpace); + // if (pose) { + // const p = pose.transform.position; + + // // In mobile AR, we only have one view. + // const view = pose.views[0]; + // const viewport = session.renderState.baseLayer!.getViewport(view)!; + + // // Use the view's transform matrix and projection matrix + // // const viewMatrix = mat4.invert(mat4.create(), view.transform.matrix); + // const viewMatrix = view.transform.inverse.matrix; + // const projectionMatrix = view.projectionMatrix; + // } + + // // Queue up the next draw request. + // session.requestAnimationFrame(onXRFrame); + // }; + + // session.requestAnimationFrame(onXRFrame); + } + } + + setReferenceSpaceType(referenceSpaceType: XRReferenceSpaceType) { + this.referenceSpaceType = referenceSpaceType; + } + + private onSessionEnd = () => { + this.session.removeEventListener('end', this.onSessionEnd); + }; +} diff --git a/packages/g-webgl/src/index.ts b/packages/g-webgl/src/index.ts index 4a86426c2..e1285afc6 100644 --- a/packages/g-webgl/src/index.ts +++ b/packages/g-webgl/src/index.ts @@ -5,8 +5,11 @@ import * as DomInteraction from '@antv/g-plugin-dom-interaction'; import * as HTMLRenderer from '@antv/g-plugin-html-renderer'; import * as ImageLoader from '@antv/g-plugin-image-loader'; import { ContextRegisterPlugin } from './ContextRegisterPlugin'; +import { WebXRManager } from './WebXRManager'; export { DomInteraction, DeviceRenderer, HTMLRenderer }; +export { ARButton } from './ARButton'; +export { WebXRManager } from './WebXRManager'; export interface WebGLRendererConfig extends RendererConfig { targets: ('webgl1' | 'webgl2')[]; @@ -17,6 +20,8 @@ export interface WebGLRendererConfig extends RendererConfig { } export class Renderer extends AbstractRenderer { + xr: WebXRManager; + constructor(config?: Partial) { super({ enableSizeAttenuation: false, @@ -24,6 +29,7 @@ export class Renderer extends AbstractRenderer { }); const deviceRendererPlugin = new DeviceRenderer.Plugin(config); + this.xr = new WebXRManager(deviceRendererPlugin); this.registerPlugin( new ContextRegisterPlugin(deviceRendererPlugin, config), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c020f1fa..f3be6a19e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,6 +354,9 @@ importers: '@types/rbush': specifier: ^3.0.0 version: 3.0.0 + '@types/webxr': + specifier: 0.5.5 + version: 0.5.5 packages/g-lottie-player: dependencies: @@ -742,6 +745,9 @@ importers: '@types/offscreencanvas': specifier: ^2019.6.4 version: 2019.6.4 + '@types/webxr': + specifier: 0.5.5 + version: 0.5.5 glslify-import: specifier: 3.1.0 version: 3.1.0 @@ -1069,6 +1075,9 @@ importers: '@types/offscreencanvas': specifier: ^2019.6.4 version: 2019.6.4 + '@types/webxr': + specifier: 0.5.5 + version: 0.5.5 packages/g-webgpu: dependencies: @@ -2633,6 +2642,10 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: true + /@types/webxr@0.5.5: + resolution: {integrity: sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==} + dev: true + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true