如何将 texture/shape 投影到 Three.js 中网格的特定部分?

How to project a texture/shape to specific part of a mesh in Three.js?

我想将纹理或某种形状(透明 - 环形、圆形)投射到网格(但投射到特定部分)。例如,在游戏中我们 select/click 一个敌人或 NPC 然后我们会在角色下方看到一些圆圈,表示选择。该圆根据网格(高度,坡度)改变其形状,您可以查看以下图像。

我想这样做,但我不确定如何完美地做到这一点 - 到目前为止,我尝试对网格进行光线投射并使顶点正常并应用旋转但是当涉及到更复杂的问题时部分网格效果不佳。我想我需要使用着色器?有什么资源可以查吗?

并不像看起来那么难。

将地面material修改为.onBeforeCompile,通过selected物体的位置统一,然后在着色器中处理。

在代码片段中,单击一个按钮 select 相应的对象,因此 select 离子标记将跟随它在地面上:

body{
  overflow: hidden;
  margin: 0;
}
#selections {
  width: 100px;
  display: flex;
  flex-direction: column;
}
button.selected{
  color: #00ff32;
  background: blue;
}
<div id="selections" style="position: absolute;border: 1px solid yellow;"></div>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.133";
import {
  OrbitControls
} from "https://cdn.skypack.dev/three@0.133/examples/jsm/controls/OrbitControls.js";

let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 5, 8);
camera.lookAt(scene.position);
let renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x404040);
document.body.appendChild(renderer.domElement);

let controls = new OrbitControls(camera, renderer.domElement);

let light = new THREE.DirectionalLight(0xffffff, 1);
light.position.setScalar(1);
scene.add(
    light,
  new THREE.AmbientLight(0xffffff, 0.5)
);

let objects = new Array(5).fill(0).map((p,idx)=>{return setObject(idx)});
//console.log(objects);
let selected = objects[0];

let g = new THREE.PlaneGeometry(10, 10, 5, 5);
g.rotateX(-Math.PI * 0.5);
for(let i = 0; i < g.attributes.position.count; i++){
    g.attributes.position.setY(i, (Math.random() * 2 - 1) * 0.75);
}
g.computeVertexNormals();
let uniforms = {
    selection: {value: new THREE.Vector3()}
}
let m = new THREE.MeshLambertMaterial({
    color: 0x003264,
    map: new THREE.TextureLoader().load("https://threejs.org/examples/textures/water.jpg"),
  onBeforeCompile: shader => {
    shader.uniforms.selection = uniforms.selection;
    shader.vertexShader = `
        varying vec3 vPos;
      ${shader.vertexShader}
    `.replace(
        `#include <begin_vertex>`,
      `#include <begin_vertex>
        vPos = transformed;
      `
    );
    shader.fragmentShader = `
        #define ss(a, b, c) smoothstep(a, b, c)
        uniform vec3 selection;
      varying vec3 vPos;
      ${shader.fragmentShader}
    `.replace(
        `#include <dithering_fragment>`,
      `#include <dithering_fragment>
      
        // shape
        float dist = distance(selection.xz, vPos.xz);
        float r = 0.25;
        
        float shape = (ss(r-0.1, r, dist)*0.75 + 0.25) - ss(r, r + 0.1, dist);
        
        vec3 col = mix(gl_FragColor.rgb, vec3(0, 1, 0.25), shape);
        gl_FragColor = vec4(col, gl_FragColor.a);
      `
    );
    //console.log(shader.fragmentShader)
  }
});

let o = new THREE.Mesh(g, m);
scene.add(o);

window.addEventListener("resize", onResize);

let clock = new THREE.Clock();

renderer.setAnimationLoop(_ => {
    
  let t = clock.getElapsedTime() * 0.5;
  
  objects.forEach(obj => {
    let ud = obj.userData;
    obj.position.x = Math.cos(t * ud.scaleX + ud.initPhase) * 4.75;
    obj.position.y = 1;
    obj.position.z = Math.sin(t * ud.scaleZ + ud.initPhase) * 4.75;
  })
  
  o.worldToLocal(uniforms.selection.value.copy(selected.position));
  
  renderer.render(scene, camera);
  
})

function setObject(idx){
    let g = new THREE.SphereGeometry(0.25);
  let m = new THREE.MeshLambertMaterial({color: 0x7f7f7f * Math.random() + 0x7f7f7f});
  let o = new THREE.Mesh(g, m);
  o.userData = {
    initPhase: Math.PI * 2 * Math.random(),
    scaleX: Math.random() * 0.5 + 0.5,
    scaleZ: Math.random() * 0.5 + 0.5
  }
  scene.add(o);
  
  let btn = document.createElement("button");
  btn.innerText = "Object " + idx;
  selections.appendChild(btn);
  btn.addEventListener("click", event => {
    selections.querySelectorAll("button").forEach(b => {b.classList.remove("selected")});
    btn.classList.add("selected");
    selected = o
  });
  
  return o;
}

function onResize(event) {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
}

</script>