Skip to main content

Draw Cloud

一个简单的云体渲染示例,这里主要用到了实例化渲染技术。

一个简单的示例

Result
Loading...
Live Editor
function render(props) {
  const drawVertex = `attribute vec2 uv;
    attribute vec3 position;

    attribute vec3 offset;
    attribute vec3 random;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;
    varying vec2 vUv;

    void rotate2d(inout vec2 v, float a){
        mat2 m = mat2(cos(a), -sin(a), sin(a),  cos(a));
        v = m * v;
    }

    void main() {
        vUv = uv;

        // copy position so that we can modify the instances
        vec3 pos = position;

        pos *= 0.5 + random.z * random.z * 1.5;

        // rotate around y axis
        rotate2d(pos.xz, random.y / 2.0);

        // rotate around x axis just to add some extra variation
        rotate2d(pos.zy, random.x * 3.14);

        pos += offset;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
    `;

  const drawFragment = `precision highp float;

    uniform sampler2D texture;
    uniform vec3 u_fogColor;
    uniform float u_fogNear;
    uniform float u_fogFar;

    varying vec2 vUv;

    void main() {
      float depth = gl_FragCoord.z / gl_FragCoord.w;
      float fogFactor = smoothstep(u_fogNear, u_fogFar, depth);
      vec4 tex = texture2D(texture, vUv);
      gl_FragColor = tex;
      gl_FragColor.w *= pow(gl_FragCoord.z, 20.0);
			gl_FragColor = mix(gl_FragColor, vec4(u_fogColor, gl_FragColor.w), fogFactor);
			// gl_FragColor = vec4(0.5, 1.0, 0.5, 1.0);
    }
    `;

  const refDom = useRef(null);
  const meshRef = useRef(null);
  const cameraRef = useRef(null);
  const renderRef = useRef(null);
  const mousePos = useRef([0, 0]);

  const store = leva.useCreateStore();

  const fov = 30;
  const nearZ = 1;

  const farZ = 3000;

  const config = {
    fov: {
      value: fov,
      min: -50,
      max: 50,
      step: 1,
      onChange: (fov) => {
        if (cameraRef.current) {
          cameraRef.current.fov = fov;
        }
      },
    },
    nearZ: {
      value: nearZ,
      min: -50,
      max: 50,
      step: 0.1,
      onChange: (nearZ) => {
        if (cameraRef.current) {
          cameraRef.current.near = nearZ;
        }
      },
    },
    farZ: {
      value: farZ,
      min: -500,
      max: 500,
      step: 1,
      onChange: (farZ) => {
        if (cameraRef.current) {
          cameraRef.current.far = farZ;
        }
      },
    },
    cameraPosition: {
      value: [0, 0, 15],
      onChange: (p) => {
        if (cameraRef.current) {
          cameraRef.current.position.set(...p);
        }
      },
    },
    wireframe: {
      value: false,
      onChange: (p) => {
        if (meshRef.current) {
          meshRef.current.wireframe = p;
        }
      },
    }
  };

  leva.useControls(config, {
    store: store,
  });

  const image = useBaseUrl('/assets/cloud.png');

  const init = () => {
    const canvas = refDom.current;

    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
    const renderer = new Renderer(canvas, {
      alpha: true,
      antialias: true,
      premultipliedAlpha: true,
    });

    renderer.state.setClearColor(new Color(0, 0, 0, 0));

    renderRef.current = renderer;

    const camera = new PerspectiveCamera(fov, canvas.width / canvas.height, nearZ, farZ);
    cameraRef.current = camera;

    function resize(target) {
      const { width, height } = target.getBoundingClientRect();
      renderer.setSize(width, height);
      camera.aspect = width / height;
    }

    const scene = new Scene();

    const planeGeometry = new Plane(renderer, {
      width: 64,
      height: 64,

      widthSegments: 1,
      heightSegments: 1,
    });

    const texture = new Texture(renderer, {
      flipY: true,
      width: 256,
      height: 256,
      minFilter: renderer.gl.LINEAR_MIPMAP_LINEAR
    });

    texture.fromSrc(image);

    const program = new Program(renderer, {
      vertexShader: drawVertex,
      fragmentShader: drawFragment,
      uniforms: {
        texture: {
          value: texture,
        },
        u_fogColor: {
          value: [0.27058823529411763, 0.5176470588235295, 0.7058823529411765],
        },
        u_fogNear: {
          value: -100,
        },
        u_fogFar: {
          value: 3000,
        },
      },
      cullFace: renderer.gl.BACK,
      depthTest: false,
      depthWrite: false,
      transparent: true,
      blendFunc: {
        src: renderer.gl.SRC_ALPHA,
        dst: renderer.gl.ONE_MINUS_SRC_ALPHA,
        srcAlpha: renderer.gl.ONE,
        dstAlpha: renderer.gl.ONE_MINUS_SRC_ALPHA,
      },
      blendEquation: {
        modeRGB: renderer.gl.FUNC_ADD,
        modeAlpha: renderer.gl.FUNC_ADD,
      },
    });

    const num = 1000;
    let offset = new Float32Array(num * 3);
    let random = new Float32Array(num * 3);
    for (let i = 0; i < num; i++) {
      offset.set([Math.random() * 1000 - 500, -Math.random() * Math.random() * 200 - 15, i], i * 3);

      // unique random values are always handy for instances.
      // Here they will be used for rotation, scale and movement.
      random.set([Math.random(), Math.random(), Math.random()], i * 3);
    }

    const geometry = new Geometry(renderer, {
      position: planeGeometry.attributesData.position,
      uv: planeGeometry.attributesData.uv,
      normal: planeGeometry.attributesData.normal,
      index: planeGeometry.attributesData.index,
      offset: { divisor: 1, size: 3, data: offset },
      random: { divisor: 1, size: 3, data: random },
    });

    const mesh = new Mesh(renderer, {
      mode: renderer.gl.TRIANGLES,
      geometry,
      program,
    });
    mesh.setParent(scene);

    const plane = new Mesh(renderer, {
      geometry,
      program,
      wireframe: false,
    });
    plane.setParent(scene);

    meshRef.current = plane;

    const raf = new Raf((t) => {
      const position = t * 30 % 1000;
      const [mouseX, mouseY] = mousePos.current;

      camera.position.x += (mouseX - camera.position.x) * 0.01;
      camera.position.y += (-mouseY - camera.position.y) * 0.01;
      camera.position.z = -position + 1000;

      renderer.render({ scene, camera });
    });

    return {
      canvas,
      resize,
    }
  }

  function onDocumentMouseMove(event) {
    if (!refDom.current) return;
    const windowHalfX = refDom.current.clientWidth / 2;
    const windowHalfY = refDom.current.clientHeight / 2;
    const mouseX = (event.clientX - windowHalfX) * 0.25;
    const mouseY = (event.clientY - windowHalfY) * 0.15;
    mousePos.current = [mouseX, mouseY];
  }

  useEffect(() => {
    const { canvas, resize } = init();

    observe(canvas, resize);

    document.addEventListener('mousemove', onDocumentMouseMove, false);

    const cv = document.createElement('canvas');
    cv.width = 32;
    cv.height = window.innerHeight;

    const ctx = cv.getContext('2d');

    const gradient = ctx.createLinearGradient(0, 0, 0, cv.height);
    gradient.addColorStop(0, '#1e4877');
    gradient.addColorStop(0.5, '#4584b4');

    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, cv.width, cv.height);

    canvas.style.background = 'url(' + cv.toDataURL('image/png') + ')';
    canvas.style.backgroundSize = '32px 100%';

    return () => {
      unobserve(canvas, resize);
      document.removeEventListener('mousemove', onDocumentMouseMove, false);
    };
  }, []);

  return (
    <div className="live-wrap">
      <div className="leva-wrap">
        <Leva
          fill
        ></Leva>
        <LevaPanel collapsed store={store} fill></LevaPanel>
      </div>
      <canvas className="scene-canvas" ref={refDom}></canvas>
    </div>
  );
}