Last active
April 4, 2025 00:47
-
-
Save jeantimex/2eaf2b564f884f89a39f8707125cff75 to your computer and use it in GitHub Desktop.
Draw a 3D sphere by Ray Marching technique
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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