惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
G
Google Developers Blog
J
Java Code Geeks
The GitHub Blog
The GitHub Blog
F
Full Disclosure
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
酷 壳 – CoolShell
酷 壳 – CoolShell
Recent Announcements
Recent Announcements
Help Net Security
Help Net Security
The Hacker News
The Hacker News
IT之家
IT之家
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
V
Visual Studio Blog
博客园 - 聂微东
Hacker News: Ask HN
Hacker News: Ask HN
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Troy Hunt's Blog
Last Week in AI
Last Week in AI
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
Google DeepMind News
Google DeepMind News
Cloudbric
Cloudbric
N
News | PayPal Newsroom
A
About on SuperTechFans
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hugging Face - Blog
Hugging Face - Blog
M
MIT News - Artificial intelligence
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
罗磊的独立博客
K
Kaspersky official blog
The Cloudflare Blog
I
Intezer

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Real-time relighting of Gaussian Splatting reflections on iPhone (Metal)
Daisuke Majima · 2026-06-02 · via DEV Community

Daisuke Majima

I built a Metal viewer on iPhone that re-lights an already-captured 3D scene with any lighting you like. Swap or rotate the HDR environment map, and the object's reflections follow in real time, with that environment also drawn into the background.

Why relighting is valuable (the practical, commercial meaning)

Ordinary Gaussian Splatting has the lighting from capture time baked in. So if you place the captured object somewhere else, the highlights and shadows clash with the new surroundings and look fake. Relighting strips that light off and returns it to material, so you can place the captured object under any lighting. This matters in practice:

  • E-commerce / product visuals: capture a product once and show it under any light — showroom, outdoors, the customer's room (AR). Strong for "texture sells" goods like furniture, cars, jewelry, sneakers.
  • Film / virtual production: place a real capture into a new scene, consistent with that scene's lighting (HDRI / LED wall). No reshoot.
  • AR / spatial computing: an object placed in a real room only blends in once it's lit by the room's light. Relighting is a precondition for realistic AR placement.
  • Games / real-time 3D: instead of a baked-in fixed look, you get a photoreal asset that reacts to dynamic in-game light (day/night, moving lights).
  • Cost: an asset captured in minutes becomes "usable under real production lighting," like manual modeling + material authoring that takes days.

In short, relighting turns a captured 3D into an actually usable asset. This article is a record of bringing it from desktop research (CUDA-assumed) to a real-time Metal implementation on iPhone.


The first half collects background knowledge; the second half covers the implementation and four bugs I hit. I define jargon as it appears, so you can follow even without a Gaussian Splatting / PBR background.

Repo: https://github.com/john-rocky/MetalGaussianSplatRelighting


Background

1. What is 3D Gaussian Splatting

A method that reconstructs a 3D scene from a set of photos and renders it in real time. The scene is represented as a huge set of translucent ellipsoids called splats. Each splat has "position, shape & orientation (rotation), color, opacity." Rendering projects each splat to the screen as an ellipse and alpha-composites them front-to-back in depth order. Color changes with viewing angle (view-dependent).

Key point: ordinary Gaussian Splatting holds the appearance (color) itself, with the capture-time lighting baked in. So you can't change the lighting afterward.

2. Lighting and relighting

Ordinary GS directly learns "the color of that spot photographed under that light." So lighting is fixed.

Relightable GS thinks differently. Per splat, it holds not "color" but a decomposed material:

  • Albedo: the base color of the material itself, with lighting removed
  • Normal: the direction the surface faces
  • Roughness: surface micro-roughness
  • Reflectance: strength of specular reflection

With the material, you can recompute the color on the fly under any environment light. That's relighting. The Ref-Gaussian I used learns this material decomposition.


That's enough if you get "splats hold material, and we want to re-light them in a new environment." The rest, #3–#5, just answer one question:

Given that material and the environment, how do we compute the color of one pixel?


3. Reflection is split into two and added

Light hitting a surface returns in two ways:

  • Diffuse: returns light evenly in all directions → you see the albedo color itself. No reflections.
  • Specular: returns only in a specific direction (the reflection of the incidence) → the environment is reflected.

So color = diffuse + specular. The splat's reflectance decides the blend (how strong the specular is). And roughness decides how blurry the specular is:

  • low roughness → the environment reflects crisply, like a mirror
  • high roughness → blurry; the bright parts of the environment just appear as a vague blob

This matters later. In fact, a glossy car (roughness ~0.2) just shows the environment lighting as a blurry white blob that moves, without resolving beams or window shapes. It's not "no reflection," it's a blurry reflection, and that's physically correct. Drop the roughness way down and the same car becomes mirror-like, clearly reflecting the room.

4. How to hold the environment (light source)

Instead of placing point bulbs, use a 360° image as the light = a list of what color light comes from each direction.

  • HDR: an image that can hold brightness above 1 (needed because windows and lights are orders of magnitude brighter than paper)
  • equirect / cubemap: names for the storage format of that 360° image. Both contents are "direction → light color."

Swapping the environment map = swapping the lighting = relighting.

5. Getting diffuse and specular from the environment (split-sum)

We want to compute #3's "diffuse" and "specular" from #4's environment map. Done naively, you integrate the environment per pixel — heavy every frame. So UE4's split-sum precomputes two images:

  • For diffuse (irradiance): the environment averaged over all directions → one lookup in the normal direction gives diffuse light.
  • For specular (prefiltered): the environment blurred per roughness level (stored progressively in mips) → one lookup in the reflection direction gives specular light (blur level = roughness).

At runtime you just sample these two textures. Replacing a heavy integral with "look up a pre-blurred image" is the heart of split-sum. (Auxiliary: a small table that fine-tunes specular strength by angle — the BRDF LUT — is also precomputed.)

I implemented this precompute kernel in Metal.

6. Deferred shading (a splat-specific issue)

Splats are translucent and overlap, so neighboring splats' normals vary and get noisy. If you shade each splat individually and then composite, that noise comes straight through.

So change the order: first accumulate each splat's material (color, normal, roughness, etc.) into a screen buffer (G-buffer) and blend = average, then shade once per pixel. Computing after normals are averaged reduces noise. In Metal, use tile memory (imageblock) to keep this buffer on the GPU and process it fast.

7. Normals and coordinate systems

  • Normal: the unit vector of the surface direction. The reflection direction depends on it, so if it's off, all reflections are off. It's reconstructed from the splat's orientation (rotation quaternion).
  • Up-axis convention: there's Y-up (many viewers) and Z-up (Blender-family). A mismatch between data and viewer tips the object over (→ bug 3).

Implementation: the shading equations

With the background in place, read the equations Ref-Gaussian's deferred surfel renderer (render_surfel) computes per pixel:

F0       = 0.04*(1 - reflectance) + albedo * reflectance
specular = prefiltered(reflect(V, N), roughness) * (F0 * fg.x + fg.y)
final    = (1 - reflectance) * base_color + specular

  • reflect(V, N): view direction V reflected about normal N. Look up prefiltered (#5) here = the environment reflected in the specular.
  • fg: a lookup into the BRDF LUT (#5).
  • final: uses base_color directly for diffuse, and only computes specular from the environment and adds it (= #3's "diffuse + specular"). This matters in bug 2.

Whole pipeline:

Ref-Gaussian .ply --> load --> per-splat material (normal, roughness, reflectance, albedo)
                                       |
HDR env --> IBL precompute --> prefiltered (specular) + irradiance (diffuse) + BRDF LUT
                                       |
                         +-------------+--------------+
                         v                            v
                 G-buffer pass               postprocess pass
        (blend color/normal/material      (per-pixel split-sum IBL
          into tile memory = #6)            + skybox compositing)

— sounds clean. Until you run it. Now the real story: four bugs.


Bug 1: the normal map is a rainbow sandstorm

The shading was patchy and flickering. The "Normal" debug view (normals visualized as color) was rainbow noise, not a smooth gradient.

My first thought — "2D-surfel normals are just inherently noisy" — was wrong, and I nearly wasted a stack of device builds on it.

What saved me was discipline: first draw the normals offline and verify. Compositing each splat's geometric normal (#7: reconstructed from the rotation quaternion and flipped to face the camera) with a small numpy script gave a smooth result (median gradient 0.006). So the data was correct and the renderer was buggy.

Culprit: at load time MetalSplatter reorders splats for cache efficiency (Morton order, sortByLocality). It reorders the splat buffer and the SH-coefficient buffer, but the material buffer (which holds the normals) I'd added later was a separate buffer and wasn't reordered.

So after sorting, splats[i] corresponded to materials[some other j], and every splat held someone else's normal. Color (the view-dependent color of #1) was in an already-reordered buffer, so it stayed consistent, and only the normals and specular looked broken — which made it hard to isolate.

The fix was one line:

materialsBuffer.values.reorderInPlace(fromSourceIndices: sorted)

Lesson: when you bolt a "per-element parallel buffer" onto someone else's pipeline, fix every place the source data gets reordered.

Bug 2: I was re-lighting the diffuse with environment light (don't)

Even after fixing normals, the body was a "watery," patchy yellow.

I'd written the diffuse term by the textbook as albedo × irradiance (multiplying by #5's diffuse image). But Ref-Gaussian's equation uses base_color directly for diffuse — only the specular is relit. I was painting a pattern onto the cream-colored body with my own irradiance. Worse, I was tinting the specular F0 with the view-dependent color (capture-time reflections baked in) instead of the learned albedo.

Matching the reference equation exactly fixed it. Lesson: before improvising "correct" PBR, read the reference implementation's source and match it line by line.

Bug 3: the car is on its side (Z-up vs Y-up)

When I trained and loaded a reflective car, it rendered 90° on its side.

Instead of guessing, I measured the point cloud's bounding box: the shortest axis was Z (= height), the longest was Y (= length), and the dark tire splats were on the −Z side. The data was Z-up (#7, Blender-family). The viewer assumed Y-up. The 90° offset that didn't show on a round helmet was suddenly exposed by the car.

Adding a Z-up→Y-up correction (−90° about X) to the camera fixed the car. Then a second head sprouted: now the background environment was 90° off. equirect (#4) assumes Y-up, but the skybox rays and reflections are computed in the scene's Z-up frame.

The fix: convert the sample direction for the environment into the environment's Y-up frame, and rotate the slider about the scene's up-axis:

environmentRotation = Rx(-90°) * Rz(slider angle)

The skybox and reflections sample with the same matrix, so they always match. I verified the mapping numerically before building, and validated the skybox itself by an offline render that reconstructs rays from the inverse view-projection. That also caught an old top-bottom flip (double-flipped) I'd previously baked into the HDR.

Bug 4: a "successful" build that runs old code

After the orientation fix, a report came in: "the car is still on its side." The offline render had already proven the math correct, so the running binary must be stale — but why?

I'd been type-checking with xcodebuild -destination platform=macOS. That only compiles the #if os(macOS) branch and Mac architectures. When I built for the iOS simulator, existing code revealed a compile error: I was assigning Float16(x).bitPattern into a [UInt16] array.

  • arm64 (device): native Float16 exists, bitPattern is UInt16 → compiles
  • the simulator's x86_64 slice: Float16 falls back, bitPattern is UInt32 → type error

The iOS build was failing, so the device kept running the previous binary. Holding [Float16] directly fixed it.

Lesson: type-check for the platform you ship to. A macOS-only xcodebuild will happily lie about your iOS app. And "nothing changes on device" is almost always a sign the binary isn't new — suspect that before re-debugging your logic.


The methodology that actually worked

What all four bugs share: establish ground truth before judging your output. Early on, my offline numpy preview was overly smooth and misled me. What worked:

  • Run the reference renderer (Ref-Gaussian's eval.py or training-time visualizations) on the same asset and compare. If the reference is clean and yours is dirty, it's a bug in your renderer.
  • Reproduce the transforms (normals, skybox rays) exactly offline and look at them before building for device.
  • Verify on a clean synthetic asset (a hand-made chrome sphere) to separate "renderer bug" from "asset bug."

Every time I skipped this and settled for "looks fine," I was wrong.

Aside: is the look "correct"?

After finishing, I felt "the car's reflections are dull, it doesn't look like it's reflecting the environment." That's not a bug — it's the material's nature. To be clear:

  • The trained car is roughness ~0.2 = semi-gloss. Semi-gloss reflects the environment blurrily (not a mirror). So bright lighting appearing as a blurry blob is correct. Any renderer, lit by the same environment, shows it equally dull. Even the official renderer's output (ground truth) shows a clean reconstruction (sharp car, smooth normals); the material is fine.
  • Turning off the app's "Use trained material" and lowering roughness lets you override all splats to a uniform specular, clearly reflecting the room. That's flashy but not present on a real car. ON = the learned real material, OFF = a manual override.
  • Note that material decomposition has an inherent ambiguity: a blue car's albedo can decompose as yellowish (splitting blue into "blue light + yellow material"). This is normal in inverse rendering; the rendered result itself is clean, so the harm is small, but it's not a "physically perfect decomposition."

So "dull look = the material behaving correctly," not an error in the iOS renderer.

Result

A reflective Ref-Gaussian scene, relit in real time on iPhone: switch and rotate the HDR environment and the reflections and skybox follow. The base is MetalSplatter, the relighting model is Ref-Gaussian, and split-sum IBL is from UE4.

Code, demo, details: https://github.com/john-rocky/MetalGaussianSplatRelighting

Next: light the splats from the actual environment via ARKit's environment probe — place a relightable object in your own room and reflect the room.


Implemented in Swift + Metal / iOS. Credits: MetalSplatter (Sean Cier, MIT), Ref-Gaussian, HDRIs from Poly Haven (CC0). Originally published in Japanese on Qiita.