用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/arm2arm/AstroAgentAssistant --skill webxr-dev命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | webxr-dev |
| description | Build WebXR portals and AR/VR browser apps in Three.js. |
| version | 1.0.0 |
| author | Hermes Agent |
| license | MIT |
| tags | ["webxr","threejs","ar","vr","immersive-web","docker","nginx","ssl"] |
Class-level skill for building WebXR-enabled immersive web experiences — portal doors, AR overlays, VR rooms, and interactive 3D pages that run in the browser.
Triggers:
index.html, css/style.css, js/app.js, package.jsonrenderer.xr.enabled = trueimmersive-ar and immersive-vr modes, provide fallbacksrenderer.xr.setSession(), renderer.setAnimationLoop() for XR frame callbacksimmersive-ar) only works on Chrome for Android (mobile AR)immersive-vr) works on desktop Chrome with flags enabled (chrome://flags → WebXR)async enterAR() {
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['local-floor'],
optionalFeatures: ['hand-tracking', 'dom-overlay', 'hit-test'],
domOverlay: { root: document.body }
});
this.renderer.xr.setSession(session);
// Use renderer.setAnimationLoop() for XR frame callbacks
}
async enterVR() {
const session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['local-floor'],
optionalFeatures: ['bounded-floor']
});
this.renderer.xr.setSession(session);
}
if (!('xr' in navigator)) {
// No WebXR — show disabled button
} else {
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
if (supported) { /* enable AR button */ }
else {
// Try VR as fallback
navigator.xr.isSessionSupported('immersive-vr').then(vrSupported => {
if (vrSupported) { /* enable VR button instead */ }
else { /* show not supported */ }
});
}
});
}
Use custom GLSL shaders for portal effects — vertex displacement + fragment color mixing with time uniforms:
// Vertex: displace Z based on sin/cos of position + time
// Fragment: mix colors based on UV coords + time + spiral distance
Generate on host, COPY into image. Never generate inside Dockerfile — openssl in Alpine exits code 1 silently:
mkdir -p ssl/
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/server.key -out ssl/server.crt \
-subj "/CN=localhost" 2>/dev/null
Serve both HTTP (for dev) and HTTPS (for WebXR):
server { listen 8123; ... } # HTTP for dev
server { listen 8124 ssl; ... } # HTTPS for WebXR
Dockerfile: EXPOSE 8123 8124, run with -p 8123:8123 -p 8124:8124
After COPY, files are root:root with mode 600. Fix:
docker exec <container> chown -R nginx:nginx /usr/share/nginx/html
docker exec <container> find /usr/share/nginx/html -type d -exec chmod 755 {} \;
docker exec <container> find /usr/share/nginx/html -type f -exec chmod 644 {} \;
add_header Cross-Origin-Embedder-Policy require-corp always;
add_header Cross-Origin-Opener-Policy same-origin always;
add_header Cross-Origin-Resource-Policy cross-origin always;
HAProxy must add cross-origin headers per-domain using rspdel + rspadd.
CRITICAL: A blanket Permissions-Policy: camera=() in frontend config blocks camera for ALL domains.
# Remove blanket headers for ar.aip.de
rspdel ^Permissions-Policy:\ if { ssl_fc_sni ar.aip.de }
rspdel ^Cross-Origin-Embedder-Policy:\ if { ssl_fc_sni ar.aip.de }
rspdel ^Cross-Origin-Opener-Policy:\ if { ssl_fc_sni ar.aip.de }
rspdel ^Cross-Origin-Resource-Policy:\ if { ssl_fc_sni ar.aip.de }
# Add domain-specific headers
rspadd Cross-Origin-Embedder-Policy:\ require-corp\ if { ssl_fc_sni ar.aip.de }
rspadd Cross-Origin-Opener-Policy:\ same-origin\ if { ssl_fc_sni ar.aip.de }
rspadd Cross-Origin-Resource-Policy:\ cross-origin\ if { ssl_fc_sni ar.aip.de }
rspadd Permissions-Policy:\ geolocation=(),\ microphone=(),\ camera=(self)\ if { ssl_fc_sni ar.aip.de }
Also set forwarded headers:
http-request set-header X-Forwarded-Proto https
http-request set-header Host ar.aip.de
Verify: curl -sI https://ar.aip.de | grep -iE "permissions-policy|cross-origin"
For React-based XR, use React Three Fiber + @react-three/xr (Vite build):
git clone https://github.com/WawasCode/DefaultReactXR.git
cd DefaultReactXR && pnpm install && pnpm build
# Build outputs to dist/ — serve with any static server
Key: fix Vite config for newer @vitejs/plugin-react (remove babel key).
Set pnpm config set allow-scripts true to allow esbuild build scripts.
Use Three.js 0.160.0+ for WebXR projects. Older versions (0.126.0, 0.150.0) have unreliable XR passthrough, broken renderer.xr handling, and missing features like proper reference space management.
<!-- CORRECT — modern version with full XR support -->
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<!-- WRONG — unreliable XR passthrough -->
<script src="https://unpkg.com/three@0.126.0/build/three.js"></script>
Renderer initialization order is device/browser-dependent. Some browsers require renderer BEFORE session, others AFTER. If passthrough shows black screen, try the opposite order.
Pattern A: Renderer BEFORE session (Chrome Android typical):
// Create renderer with XR support
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true,
preserveDrawingBuffer: false,
powerPreference: 'high-performance'
});
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType('local-floor');
renderer.autoClear = false; // XR compositor manages clearing
// THEN request session
session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'local-floor'],
optionalFeatures: ['dom-overlay'],
domOverlay: { root: uiLayer }
});
renderer.xr.setSession(session);
Pattern B: Renderer AFTER session (if Pattern A shows black):
// Request session FIRST
session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'local-floor'],
optionalFeatures: ['dom-overlay'],
domOverlay: { root: uiLayer }
});
// THEN create renderer with active session
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true,
preserveDrawingBuffer: false
});
renderer.xr.enabled = true;
renderer.xr.setSession(session); // Bind immediately
Critical settings for passthrough:
preserveDrawingBuffer: false — let XR compositor manage framebufferautoClear: false — XR session handles clearing, not Three.jsrenderer.setClearColor() — avoid or use with alpha=0scene.background = null — no background color/texturebackground: transparent !importantBLOCKS PASSTHROUGH (do NOT do any of these):
// ❌ Manual camera matrix overrides during XR frame
camera.matrixAutoUpdate = false;
camera.projectionMatrix.fromArray(view.projectionMatrix);
// ❌ Manual framebuffer clear
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// ❌ Non-transparent body/canvas backgrounds in CSS
body { background: #0a1929; } // ← leaks through in AR
Transparent background in AR: Dark CSS body backgrounds leak through the composited view. Use a CSS class toggle + inline styles:
// On AR entry:
document.body.style.background = 'transparent';
canvas.style.background = 'transparent';
document.body.classList.add('ar-active');
// In CSS — strip dark backgrounds from DOM overlays:
body.ar-active .header,
body.ar-active #ui-layer {
background: transparent !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
// On session end — restore:
document.body.style.background = '';
document.body.classList.remove('ar-active');
DOM overlays in XR: Elements that should appear in XR (scanning overlays, reticle guides, status) must be children of the domOverlay.root element. Elements outside it are not composited.
// WRONG — scanning overlay outside domOverlay root, won't render in XR
<div id="scanning-overlay">...</div> <!-- sibling of #ui-layer → NOT composited -->
// CORRECT — scanning overlay INSIDE domOverlay root
<div id="ui-layer">
<header>...</header>
<div id="scanning-overlay">...</div> <!-- child of #ui-layer → composited -->
</div>
Use opencode as the primary coding agent for WebXR projects. Configure opencode with a custom OpenAI-compatible provider and run iterative tasks via opencode run in the project directory.
openssl in Alpine exits code 1 for obscure reasons. Generate on host, COPY into image.add_header is NOT allowed inside if ($request_method = 'OPTIONS') blocks in nginx. Move all add_header directives to server {} or location {} level.nginx:nginx. Fix ownership and permissions after COPY.immersive-ar only works on Chrome for Android. Samsung Browser may not support WebXR — test with Chrome.alert() in error handlers is non-UX-friendly — use styled DOM overlay instead.renderer.setAnimationLoop() — not requestAnimationFrame() during XR sessions.-k to wget.gl.clear(), camera.matrixAutoUpdate = false, manual camera matrix overrides, non-transparent body background, or DOM elements outside domOverlay.root. See "AR Camera Passthrough" section.<meta http-equiv="Cache-Control"> tags and nginx add_header Cache-Control "no-cache" to force reload on mobile. Use version query strings (style.css?v=20260730) for CSS/JS files.MeshBasicMaterial or MeshStandardMaterial with textures instead of custom fragment shaders. Test on target device early.let glowRing = null; at module level, then assign glowRing = new THREE.Mesh(...) inside init function. Three distinct failure modes (all crash AR at runtime, none caught by node --check):
let portal = null; at top, but no portal = new THREE.Mesh(...) anywhere in setup. Any handler doing portal.parent (e.g. tap-to-place) throws TypeError: Cannot read properties of null (reading 'parent'). Grep the setup to confirm every outer-scope let actually receives an assignment.const shadows the outer let — const portalSceneGroup = new THREE.Group(); inside activateXR() re-declares a new local that shadows the outer let portalSceneGroup = null;. The top-level render() still reads null → ReferenceError/TypeError once the portal is placed, killing the animation loop. When you intend to fill an outer-scope variable, ASSIGN (portalGroup = new THREE.Group()), never const-redeclare..parent — if portal (the mesh) is added to portalGroup, portal.parent works only after portal is assigned. Safer: keep a top-level let portalGroup = null; and use it directly instead of deriving from portal.parent.
Quick verification after an AR refactor: grep -nE "let portal|portal = new THREE.Mesh|portalGroup = new|portalSceneGroup = " and eyeball that each outer let has a matching assignment. node --check only catches syntax, never scoping/runtime null-derefs.TextureLoader.load() to catch CORS or network issues on mobile.When AR button appears unresponsive on mobile:
Add visible status messages — don't rely on console.log. Show each step in the DOM:
setStatus('WebXR detected, initializing...');
setStatus('✓ WebXR available, checking AR...');
setStatus('✓ AR supported, starting session...');
Add alert() for button click confirmation — temporary debug to verify event listener works:
startButton.addEventListener('click', () => {
alert('Button clicked! navigator.xr: ' + !!navigator.xr);
activateXR();
});
Force cache refresh — tell user to:
?v=2 to URL: https://ar.aip.de?v=2Check for Samsung Browser vs Chrome — Samsung Browser may lack WebXR support. Recommend Chrome + ARCore.
Graceful degradation — if immersive-ar fails, try immersive-vr fallback or show clear error message with device requirements.
Syntax errors prevent all JavaScript — duplicate variable declarations (const glowMaterial twice) cause silent script failure. Validate with node --check before deploying.
Camera passthrough troubleshooting — if screen stays black after session starts:
preserveDrawingBuffer: false and autoClear: false