# SSGI Spheres With Rapier Physics

Demo: https://pmndrs.github.io/examples/examples/ssgi-spheres-with-rapier-physics
Source: https://github.com/pmndrs/examples/tree/main/examples/ssgi-spheres-with-rapier-physics
Scaffold: npx degit pmndrs/examples/examples/ssgi-spheres-with-rapier-physics
Published: 2023-11-12
Authors: Paul Henschel
Tags: ssgi, physics, postprocessing, environment
Ported from: https://codesandbox.io/s/5w35n6
Dependencies: @babel/runtime@^7.25.0, @react-three/drei@10.7.8, @react-three/fiber@9.6.1, @react-three/rapier@^2.2.0, maath@^0.10.8, postprocessing@6.36.6, react@19.2.8, react-dom@19.2.8, three@0.165.0
Too large to inline, in the repository: src/realism-effects/index.js (243 kB), src/realism-effects/v2.js (252 kB)

## src/App.tsx

```tsx
import * as THREE from "three";
import { type ReactNode, useRef, useReducer, useMemo } from "react";
import { Canvas, useFrame, type CanvasProps } from "@react-three/fiber";
import { Environment, Lightformer } from "@react-three/drei";
import {
  BallCollider,
  Physics,
  RigidBody,
  type RapierRigidBody,
} from "@react-three/rapier";
import { easing } from "maath";
import { Effects } from "./Effects";

const accents = ["#ff4060", "#ffcc00", "#20ffa0", "#4060ff"];
const shuffle = (accent = 0) => [
  { color: "#444", roughness: 0.1, metalness: 0.5 },
  { color: "#444", roughness: 0.1, metalness: 0.5 },
  { color: "#444", roughness: 0.1, metalness: 0.5 },
  { color: "white", roughness: 0.1, metalness: 0.1 },
  { color: "white", roughness: 0.1, metalness: 0.1 },
  { color: "white", roughness: 0.1, metalness: 0.1 },
  { color: accents[accent], roughness: 0.1, accent: true },
  { color: accents[accent], roughness: 0.1, accent: true },
  { color: accents[accent], roughness: 0.1, accent: true },
  { color: "#444", roughness: 0.1 },
  { color: "#444", roughness: 0.3 },
  { color: "#444", roughness: 0.3 },
  { color: "white", roughness: 0.1 },
  { color: "white", roughness: 0.2 },
  { color: "white", roughness: 0.1 },
  {
    color: accents[accent],
    roughness: 0.1,
    accent: true,
    transparent: true,
    opacity: 0.5,
  },
  { color: accents[accent], roughness: 0.3, accent: true },
  { color: accents[accent], roughness: 0.1, accent: true },
];

export default function App(props: CanvasProps) {
  const [accent, click] = useReducer((state) => ++state % accents.length, 0);
  const connectors = useMemo(() => shuffle(accent), [accent]);
  return (
    <Canvas
      flat
      shadows
      onClick={click}
      dpr={[1, 1.5]}
      gl={{ antialias: false }}
      camera={{ position: [0, 0, 30], fov: 17.5, near: 10, far: 40 }}
      {...props}
    >
      <color attach="background" args={["#141622"]} />
      <Physics /*debug*/ timeStep="vary" gravity={[0, 0, 0]}>
        <Pointer />
        {connectors.map((props, i) => (
          <Sphere key={i} {...props} />
        ))}
      </Physics>
      <Environment resolution={256}>
        <group rotation={[-Math.PI / 3, 0, 1]}>
          <Lightformer
            form="circle"
            intensity={100}
            rotation-x={Math.PI / 2}
            position={[0, 5, -9]}
            scale={2}
          />
          <Lightformer
            form="circle"
            intensity={2}
            rotation-y={Math.PI / 2}
            position={[-5, 1, -1]}
            scale={2}
          />
          <Lightformer
            form="circle"
            intensity={2}
            rotation-y={Math.PI / 2}
            position={[-5, -1, -1]}
            scale={2}
          />
          <Lightformer
            form="circle"
            intensity={2}
            rotation-y={-Math.PI / 2}
            position={[10, 1, 0]}
            scale={8}
          />
          <Lightformer
            form="ring"
            color="#4060ff"
            intensity={80}
            onUpdate={(self) => self.lookAt(0, 0, 0)}
            position={[10, 10, 0]}
            scale={10}
          />
        </group>
      </Environment>
      <Effects />
    </Canvas>
  );
}

type SphereProps = {
  position?: [number, number, number];
  children?: ReactNode;
  vec?: THREE.Vector3;
  scale?: number;
  r?: (range: number) => number;
  accent?: boolean;
  color?: string;
  roughness?: number;
  metalness?: number;
  transparent?: boolean;
  opacity?: number;
};

function Sphere({
  position,
  children,
  vec = new THREE.Vector3(),
  scale,
  r = THREE.MathUtils.randFloatSpread,
  accent,
  color = "white",
  ...props
}: SphereProps) {
  const api = useRef<RapierRigidBody>(null!);
  const ref = useRef<
    THREE.Mesh<THREE.SphereGeometry, THREE.MeshStandardMaterial>
  >(null!);
  const pos = useMemo<[number, number, number]>(
    () => position || [r(10), r(10), r(10)],
    [],
  );
  useFrame((state, delta) => {
    delta = Math.min(0.1, delta);
    api.current?.applyImpulse(
      vec.copy(api.current.translation()).negate().multiplyScalar(0.2),
      false,
    );
    easing.dampC(ref.current.material.color, color, 0.2, delta);
  });
  return (
    <RigidBody
      linearDamping={4}
      angularDamping={1}
      friction={0.1}
      position={pos}
      ref={api}
      colliders={false}
    >
      <BallCollider args={[1]} />
      <mesh ref={ref} castShadow receiveShadow>
        <sphereGeometry args={[1, 64, 64]} />
        <meshStandardMaterial {...props} />
        {children}
      </mesh>
    </RigidBody>
  );
}

function Pointer({ vec = new THREE.Vector3() }: { vec?: THREE.Vector3 }) {
  const ref = useRef<RapierRigidBody>(null!);
  useFrame(({ mouse, viewport }) =>
    ref.current?.setNextKinematicTranslation(
      vec.set(
        (mouse.x * viewport.width) / 2,
        (mouse.y * viewport.height) / 2,
        0,
      ),
    ),
  );
  return (
    <RigidBody
      position={[0, 0, 0]}
      type="kinematicPosition"
      colliders={false}
      ref={ref}
    >
      <BallCollider args={[1]} />
    </RigidBody>
  );
}
```

## src/Effects.tsx

```tsx
import { useThree, useFrame } from "@react-three/fiber";
import {
  EffectComposer,
  RenderPass,
  EffectPass,
  BloomEffect,
  ToneMappingEffect,
  FXAAEffect,
} from "postprocessing";
import { useEffect, useState } from "react";
import { SSGIEffect, VelocityDepthNormalPass } from "./realism-effects/v2";

// The component renders nothing; `undefined` (not `void`) keeps it a valid JSX component.
export function Effects(): undefined {
  const gl = useThree((state) => state.gl);
  const scene = useThree((state) => state.scene);
  const camera = useThree((state) => state.camera);
  const size = useThree((state) => state.size);
  const [composer] = useState(
    () => new EffectComposer(gl, { multisampling: 0 }),
  );

  useEffect(() => composer.setSize(size.width, size.height), [composer, size]);
  useEffect(() => {
    const config = {
      importanceSampling: true,
      steps: 20,
      refineSteps: 4,
      spp: 1,
      resolutionScale: 1,
      missedRays: false,
      distance: 5.980000000000011,
      thickness: 2.829999999999997,
      denoiseIterations: 1,
      denoiseKernel: 3,
      denoiseDiffuse: 25,
      denoiseSpecular: 25.54,
      radius: 11,
      phi: 0.5760000000000001,
      lumaPhi: 20.651999999999997,
      depthPhi: 23.37,
      normalPhi: 26.087,
      roughnessPhi: 18.477999999999998,
      specularPhi: 7.099999999999999,
      envBlur: 0.8,
    };

    const renderPass = new RenderPass(scene, camera);
    const velocityDepthNormalPass = new VelocityDepthNormalPass(scene, camera);
    composer.addPass(renderPass);
    composer.addPass(velocityDepthNormalPass);
    composer.addPass(
      new EffectPass(
        camera,
        new SSGIEffect(composer, scene, camera, {
          ...config,
          velocityDepthNormalPass,
        }),
      ),
    );
    composer.addPass(
      new EffectPass(
        camera,
        new BloomEffect({
          mipmapBlur: true,
          luminanceThreshold: 0.1,
          intensity: 0.9,
          levels: 7,
        }),
      ),
    );
    composer.addPass(
      new EffectPass(camera, new FXAAEffect(), new ToneMappingEffect()),
    );

    return () => {
      composer.removeAllPasses();
    };
  }, [composer, camera, scene]);

  useFrame((state, delta) => {
    gl.autoClear = true; // ?
    composer.render(delta);
  }, 1);
}
```

## src/index.tsx

```tsx
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";

createRoot(document.getElementById("root")!).render(<App />);
```

## src/realism-effects/v2.d.ts

```ts
// Hand-written types for the vendored `realism-effects` v2 bundle (`v2.js`).
// Only the two exports this example imports are declared here.
import { Effect, EffectComposer, Pass } from "postprocessing";
import { Camera, Scene, Texture } from "three";

export class VelocityDepthNormalPass extends Pass {
  constructor(scene: Scene, camera: Camera);
  get texture(): Texture;
  get depthTexture(): Texture;
}

export interface SSGIOptions {
  mode?: "ssgi" | "ssr" | "ssdgi";
  distance?: number;
  thickness?: number;
  denoiseIterations?: number;
  denoiseKernel?: number;
  denoiseDiffuse?: number;
  denoiseSpecular?: number;
  radius?: number;
  phi?: number;
  lumaPhi?: number;
  depthPhi?: number;
  normalPhi?: number;
  roughnessPhi?: number;
  specularPhi?: number;
  envBlur?: number;
  importanceSampling?: boolean;
  steps?: number;
  refineSteps?: number;
  spp?: number;
  resolutionScale?: number;
  missedRays?: boolean;
  outputTexture?: Texture | null;
  velocityDepthNormalPass?: VelocityDepthNormalPass;
}

export class SSGIEffect extends Effect {
  constructor(
    composer: EffectComposer,
    scene: Scene,
    camera: Camera,
    options?: SSGIOptions,
  );
}
```

## src/styles.css

```css
@import url("https://rsms.me/inter/inter.css");

* {
  box-sizing: border-box;
}

html,
body,
#root,
.container {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  background: #151515;
  color: black;
  font-family: "Inter";
}

a {
  color: black;
}

a {
  pointer-events: all;
  color: black;
  text-decoration: none;
}

svg {
  fill: black;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

canvas {
  opacity: 0;
  touch-action: none;
  animation: fade-in 5s ease 0.5s forwards;
}
```

## src/vite-env.d.ts

```ts
/// <reference types="vite/client" />
```
