Skip to content

Instantly share code, notes, and snippets.

@jeantimex
Last active April 4, 2025 00:47
Show Gist options
  • Save jeantimex/2eaf2b564f884f89a39f8707125cff75 to your computer and use it in GitHub Desktop.
Save jeantimex/2eaf2b564f884f89a39f8707125cff75 to your computer and use it in GitHub Desktop.
Draw a 3D sphere by Ray Marching technique
import * as THREE from "https://esm.sh/three";
import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls";
// Create a scene
const scene = new THREE.Scene();
// Create a camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// Create a renderer with antialiasing
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Set background color
const backgroundColor = new THREE.Color(0x3399ee);
// Add renderer-specific options for better quality
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(backgroundColor, 1);
// Add orbit controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxDistance = 10;
controls.minDistance = 2;
controls.enableDamping = true;
// Add directional light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
scene.add(light);
// Create a ray marching plane
const geometry = new THREE.PlaneGeometry();
const material = new THREE.ShaderMaterial();
const rayMarchPlane = new THREE.Mesh(geometry, material);
// Get the width and height of the near plane
const nearPlaneWidth = camera.near * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.aspect * 2;
const nearPlaneHeight = nearPlaneWidth / camera.aspect;
// Scale the ray marching plane
rayMarchPlane.scale.set(nearPlaneWidth, nearPlaneHeight, 1);
// Add uniforms
const uniforms = {
u_eps: { value: 0.0001 },
u_maxDis: { value: 1000 },
u_maxSteps: { value: 200 },
u_clearColor: { value: backgroundColor },
u_camPos: { value: camera.position },
u_camToWorldMat: { value: camera.matrixWorld },
u_camInvProjMat: { value: camera.projectionMatrixInverse },
u_lightDir: { value: light.position },
u_lightColor: { value: light.color },
u_diffIntensity: { value: 0.5 },
u_specIntensity: { value: 3 },
u_ambientIntensity: { value: 0.15 },
u_shininess: { value: 16 },
};
material.uniforms = uniforms;
// define vertex and fragment shaders and add them to the material
const vertCode = `
out vec2 vUv; // 将UV坐标传递给片元着色器
void main() {
// 计算世界空间中的视角方向
vec4 worldPos = modelViewMatrix * vec4(position, 1.0);
// 输出顶点位置
gl_Position = projectionMatrix * worldPos;
// 传递UV坐标给片元着色器,用于计算每个像素的光线方向
vUv = uv;
}`
const fragCode = `
precision highp float;
// From vertex shader
in vec2 vUv;
// From CPU
uniform vec3 u_clearColor;
uniform float u_eps;
uniform float u_maxDis;
uniform int u_maxSteps;
uniform vec3 u_camPos;
uniform mat4 u_camToWorldMat;
uniform mat4 u_camInvProjMat;
uniform vec3 u_lightDir;
uniform vec3 u_lightColor;
uniform float u_diffIntensity;
uniform float u_specIntensity;
uniform float u_ambientIntensity;
uniform float u_shininess;
float scene(vec3 p) {
// 计算点p到中心为(0,0,0)、半径为1.0的球体的有符号距离
// length(p)计算点到原点的距离,减去球体半径得到有符号距离场(SDF)值
// 负值表示在球体内部,正值表示在球体外部,0表示在球体表面
float sphereDis = length(p) - 1.0;
return sphereDis;
}
float rayMarch(vec3 ro, vec3 rd)
{
float d = 0.0; // 光线已行进的总距离
float cd = 0.0; // 当前场景距离
float lastCD = 0.0; // 上一步的场景距离,用于插值
vec3 p; // 光线当前位置
for (int i = 0; i < u_maxSteps; ++i) {
p = ro + d * rd; // 计算光线的当前位置
cd = scene(p); // 获取当前位置到最近表面的距离
// 如果已经足够接近表面或者已经行进太远,则停止循环
if (cd < u_eps || d >= u_maxDis) {
// 如果接近表面但还不够精确,使用线性插值提高精度
if (i > 0 && cd < u_eps * 10.0) {
// 使用线性插值获得更接近表面的精确位置
float t = lastCD / (lastCD - cd);
d = d - lastCD * t;
}
break;
}
lastCD = cd; // 保存本次距离用于下次插值
d += cd * 0.95; // 沿光线方向前进,使用0.95的松弛因子防止过冲
}
return d; // 返回光线行进的总距离
}
vec3 sceneCol(vec3 p) {
// 返回球体的颜色 - 纯红色
// 这里可以根据位置p实现更复杂的着色效果,比如纹理、渐变等
return vec3(1.0, 0.0, 0.0); // 红色 (R,G,B)
}
vec3 normal(vec3 p) // 改进的法线计算
{
// 使用更小的epsilon值来获得更精确的法线
float epsilon = u_eps * 0.1;
// 使用中心差分法计算法线 - 比四面体法更精确
// 通过在各个轴向上采样SDF并计算差值来获得梯度方向
vec3 n;
n.x = scene(vec3(p.x + epsilon, p.y, p.z)) - scene(vec3(p.x - epsilon, p.y, p.z));
n.y = scene(vec3(p.x, p.y + epsilon, p.z)) - scene(vec3(p.x, p.y - epsilon, p.z));
n.z = scene(vec3(p.x, p.y, p.z + epsilon)) - scene(vec3(p.x, p.y, p.z - epsilon));
// 归一化以获得单位法线向量
return normalize(n);
}
void main() {
// 从顶点着色器获取UV坐标
vec2 uv = vUv.xy;
// 从相机uniforms获取光线起点和方向
// 1. 光线起点是相机位置
vec3 ro = u_camPos;
// 2. 计算光线方向:
// - 将UV坐标(0到1)转换到NDC坐标(-1到1)
// - 使用相机投影矩阵逆矩阵转换到相机空间
// - 使用相机世界矩阵转换到世界空间
// - 最后归一化得到单位方向向量
vec3 rd = (u_camInvProjMat * vec4(uv*2.-1., 0, 1)).xyz;
rd = (u_camToWorldMat * vec4(rd, 0)).xyz;
rd = normalize(rd);
// 执行ray marching并计算光线行进的总距离
float disTravelled = rayMarch(ro, rd); // 使用归一化的光线方向
// 计算光线命中点的位置
vec3 hp = ro + disTravelled * rd;
// 获取命中点的法线方向
vec3 n = normal(hp);
if (disTravelled >= u_maxDis) { // 如果光线没有命中任何物体
gl_FragColor = vec4(u_clearColor, 1.0);
} else { // 如果光线命中了物体
// 计算光照模型
float dotNL = dot(normalize(u_lightDir), n);
float diff = max(dotNL, 0.0) * u_diffIntensity;
float spec = pow(max(dotNL, 0.0), u_shininess) * u_specIntensity;
float ambient = u_ambientIntensity;
// 将物体颜色与光照效果结合
vec3 color = u_lightColor * (sceneCol(hp) * (spec + ambient + diff));
gl_FragColor = vec4(color, 1.0); // 输出最终颜色
}
}
`
material.vertexShader = vertCode;
material.fragmentShader = fragCode;
// Add plane to scene
scene.add(rayMarchPlane);
// Needed inside update function
let cameraForwardPos = new THREE.Vector3(0, 0, -1);
const VECTOR3ZERO = new THREE.Vector3(0, 0, 0);
// Render the scene
const animate = () => {
requestAnimationFrame(animate);
// Update screen plane position and rotation
cameraForwardPos = camera.position.clone().add(camera.getWorldDirection(VECTOR3ZERO).multiplyScalar(camera.near));
rayMarchPlane.position.copy(cameraForwardPos);
rayMarchPlane.rotation.copy(camera.rotation);
renderer.render(scene, camera);
controls.update();
}
animate();
// Handle window resize
window.addEventListener('resize', () => {
// Update camera aspect ratio to match new window dimensions
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
// Recalculate the near plane dimensions after camera update
// 当窗口大小改变时,需要重新计算近平面尺寸以保持正确的视角和比例
const nearPlaneWidth = camera.near * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.aspect * 2;
const nearPlaneHeight = nearPlaneWidth / camera.aspect;
// Update the ray marching plane to match the new dimensions
// 更新ray marching平面的尺寸,确保它继续精确匹配相机视角
rayMarchPlane.scale.set(nearPlaneWidth, nearPlaneHeight, 1);
// Update renderer size
if (renderer) renderer.setSize(window.innerWidth, window.innerHeight);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment