Threejs绘制小兩伞快拿去送给你的女神

大家好!我是 [数擎 AI],一位热爱探索新技术的前端开发者,在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步! 开发领域 :前端开发 | AI 应用 | Web3D | 元宇宙
技术栈 :JavaScript、React、ThreeJs、WebGL、Go
经验经验 :6 年+ 前端开发经验,专注于图形渲染和 AI 技术
开源项目AI 简历元宇宙数字孪生

1. SDF 函数(Signed Distance Functions)

SDF 是一种通过数学公式定义形状的方式,常用于计算距离场。我们使用了几个 SDF 函数来构建图形:

  • sdfCircle: 用于绘制圆形。
  • sdfEllipse: 用于绘制椭圆形。
  • sdfLine: 用于绘制线段。

每个 SDF 函数返回一个值,表示当前像素到形状的距离。如果这个距离小于某个阈值,则表示像素在形状内部。

scss 复制代码
float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

2. 布尔操作函数

SDF 可以通过布尔运算进行组合,例如求并集、差集和交集。我们在代码中使用了以下几种操作:

  • sdfUnion: 返回两个形状的并集。
  • sdfDifference: 返回两个形状的差集。
  • sdfIntersection: 返回两个形状的交集。
css 复制代码
float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

这些运算让我们能够通过数学方式灵活地合成复杂的图形。

3. 渲染函数

render 函数负责将计算出的形状绘制到屏幕上。它通过 smoothstep 函数实现抗锯齿效果,并根据距离来调整颜色的透明度。

scss 复制代码
vec4 render(float d, vec3 color, float stroke) {
float anti = fwidth(d) * 1.0;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
return stroke < 0.000001 ? colorLayer : vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

这个函数通过逐层混合不同颜色和透明度来呈现复杂的视觉效果。

4.动态条纹

我们还使用了正弦函数 sin() 来生成动态条纹效果。sin(uv.x * 40.0) 使得图案随时间变化,创造出条纹的动感效果。

ini 复制代码
vec2 sinuv = vec2(uv.x, (sin(uv.x _ 40.0) _ 0.02 + 1.0) * uv.y);

通过改变 time 参数,这些条纹会在场景中随着时间不断变化,增强动画效果的表现力。

5. 背景与图层混合

为了让图形与背景更好地融合,我们使用了图层混合和背景颜色的处理。每个图层根据其透明度逐渐与背景颜色混合,最终得出渲染结果。

ini 复制代码
vec3 bcol = vec3(1.0, 0.8, 0.7 - 0.07 _ p.y) _ (1.0 - 0.25 * length(p));
fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);

6. Gamma 校正

为了调整最终的颜色输出并确保其符合人眼的感知,采用了 Gamma 校正。通过将颜色值提升到 1.0 / 2.2 的幂次方,我们可以得到更为自然的视觉效果。

ini 复制代码
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));

7. 完整代码

ini 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 1. 初始化Three.js基础场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000,
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 顶点着色器
const vertexShader = `
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelMatrix * viewMatrix * vec4(position, 1.0);
}
`;

// 片元着色器
const fragmentShader = `
uniform vec2 resolution;
uniform float time;
varying vec2 vUv;

float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

float sdfEllipse(vec2 center, float a, float b, vec2 coord) {
  float a2 = a * a;
  float b2 = b * b;
  return (b2 * (coord.x - center.x) * (coord.x - center.x) +
         a2 * (coord.y - center.y) * (coord.y - center.y) - a2 * b2)/(a2 * b2);
}

float sdfLine(vec2 p0, vec2 p1, float width, vec2 coord) {
  vec2 dir0 = p1 - p0;
  vec2 dir1 = coord - p0;
  float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0);
  return (length(dir1 - dir0 * h) - width * 0.5);
}

float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

vec4 render(float d, vec3 color, float stroke) {
  float anti = fwidth(d) * 1.0;
  vec4 strokeLayer = vec4(vec3(0.05), 1.0-smoothstep(-anti, anti, d - stroke));
  vec4 colorLayer = vec4(color, 1.0-smoothstep(-anti, anti, d));
  return stroke < 0.000001 ? colorLayer : 
    vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

void main() {
  float size = min(resolution.x, resolution.y);
  float pixSize = 1.0 / size;
  vec2 uv = vUv;
  float stroke = pixSize * 1.5;
  
  // 适配宽高比
  float aspect = resolution.y / resolution.x;
  vec2 center = vec2(0.5, 0.5 * aspect);

  // 主要形状
  float a = sdfEllipse(vec2(0.5, center.y*2.0-0.34), 0.25, 0.25, uv);
  float b = sdfEllipse(vec2(0.5, center.y*2.0+0.03), 0.8, 0.35, uv);
  b = sdfIntersection(a, b);
  vec4 layer1 = render(b, vec3(0.32, 0.56, 0.53), fwidth(b) * 2.0);

  // 动态条纹
  vec4 layer2 = layer1;
  vec2 sinuv = vec2(uv.x, (sin(uv.x*40.0)*0.02 + 1.0)*uv.y);
  for (float i = 0.0; i < 10.0; i++) {
    float t = mod(time + 0.3 * i, 3.0) * 0.2;
    float r0 = (t - 0.15)/0.2 * 0.9 + 0.1;
    float r1 = (t - 0.15)/0.2 * 0.1 + 0.9;
    float r2 = (t - 0.15)/0.2 * 0.15 + 0.85;
    
    float e = sdfEllipse(vec2(0.5, center.y*2.0+0.37-t*r2), 0.7*r0, 0.35*r1, sinuv);
    float f = sdfEllipse(vec2(0.5, center.y*2.0+0.41-t), 0.7*r0, 0.35*r1, sinuv);
    f = sdfDifference(e, f);
    f = sdfIntersection(f, b);
    vec4 layer = render(f, vec3(1.0, 0.81, 0.27), 0.0);
    layer2 = mix(layer2, layer, layer.a);
  }

  // 手柄绘制
  float bottom = 0.08;
  float handleWidth = 0.01;
  float handleRadius = 0.04;
  float d = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius, uv);
  float c = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius-handleWidth, uv);
  d = sdfDifference(d, c);
  c = uv.y - bottom;
  d = sdfIntersection(d, c);
  c = sdfLine(vec2(0.5, center.y*2.0-0.05), vec2(0.5, bottom), handleWidth, uv);
  d = sdfUnion(d, c);
  c = sdfCircle(vec2(0.5, center.y*2.0-0.05), 0.01, uv);
  d = sdfUnion(c, d);
  c = sdfCircle(vec2(0.5-handleRadius*2.0+handleWidth, bottom), handleWidth*0.5, uv);
  d = sdfUnion(c, d);
  vec4 layer0 = render(d, vec3(0.404, 0.298, 0.278), stroke);

  // 背景混合
  vec2 p = (2.0*gl_FragCoord.xy-resolution.xy)/min(resolution.y, resolution.x);
  vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p));
  vec4 fragColor = vec4(bcol, 1.0);
  
  // 图层混合
  fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
  fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
  fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);
  
  // Gamma 校正
  fragColor.rgb = pow(fragColor.rgb, vec3(1.0/2.2));
  gl_FragColor = fragColor;
}
`;

// Three.js 材质创建
const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    resolution: {
      value: new THREE.Vector2(window.innerWidth, window.innerHeight),
    },
    time: { value: 0 },
  },
});

// 3. 创建全屏平面并应用材质
const geometry = new THREE.PlaneGeometry(10, 10);
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 4. 相机位置和控制器
camera.position.z = 10;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;

// 5. 响应窗口大小变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  shaderMaterial.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height,
  );
});

// 6. 动画循环
function animate() {
  requestAnimationFrame(animate);
  material.uniforms.time.value = performance.now() / 1000;
  renderer.render(scene, camera);
}
animate();

总结

利用了 SDF 技术绘制了多个形状,并通过布尔运算组合它们,进一步通过动态条纹和动画效果增加了复杂度。通过理解这些 Shader 代码,你将能更好地掌控图形渲染的细节,并应用到更复杂的 Three.js 项目中。

相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript