将 three.js 生成的 canvas 保存到服务器 - 没有文件被保存

Saving a three.js generated canvas to server - no file being saved

我正在尝试将图像保存到使用以下 three.js 脚本创建的服务器..

actualCode(THREE);

function actualCode(THREE) {
    //Variables for rendering
    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);

    //Object variables
    let texture;
    let paintedMug;

    //Preload image, then trigger rendering
    const loader = new THREE.TextureLoader();
    texture = loader.load("images/djmug2.jpg", function (_tex) {
        // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
        init();

        //views 17.5=front | 355=side | 139.6=back
        renderImageSolo(17.5);

    });

    function init() {
        //Init scene and camera
        camera.position.set(0, 1.3, 11);
        camera.lookAt(scene.position);
        renderer.setSize(400, 400);
        
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        scene.add(light);

        //Draw white mug
        const muggeom = new THREE.CylinderGeometry(1.5, 1.5, 3.5, 240, 1);
        const mugmaterial = new THREE.MeshStandardMaterial({
            color: "#fff",
        });
        const mug = new THREE.Mesh(muggeom, mugmaterial);

        //Draw painting on mug with slightly larger radius
        const paintgeom = new THREE.CylinderGeometry(1.5001, 1.5001, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);

        //Define a group as mug + paint
        paintedMug = new THREE.Group();
        paintedMug.add(mug);
        paintedMug.add(paint);
        //Add group to scene
        scene.add(paintedMug);
    }


    function renderImageSolo(angle) {
        //Init just like main renderer / scene, will use same camera
        const solo_renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        solo_renderer.setSize(renderer.domElement.width, renderer.domElement.height);
        solo_renderer.domElement.style.marginTop = "0em"; //Space out canvas
        solo_renderer.domElement.id = "canvas"; //give canvas id
        document.body.appendChild(solo_renderer.domElement);
        const solo_scene = new THREE.Scene();
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        solo_scene.add(light);

        //Draw painting alone
        const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);
        //Add paint to scene
        solo_scene.add(paint);
        //Rotate paint by angle
        paint.rotation.y = angle
        //Draw result with green screen bg
        solo_scene.background = new THREE.Color(0x04F404);
        //Draw result with trans bg (not working showing as black atm)
        //solo_scene.background = new THREE.WebGLRenderer( { alpha: true } );

        solo_renderer.render(solo_scene, camera);
        saveit();
    }
}

然后我尝试使用 ajax 保存生成的图像,如下所示..

function saveit() {
    const canvas = document.getElementById('canvas');
    var photo = canvas.toDataURL('image/jpeg');
    $.ajax({
        method: 'POST',
        url: 'photo_upload.php',
        data: {
            photo: photo
        }
    });
}

photo_upload.php 内容..

$data = $_POST['photo'];
    list($type, $data) = explode(';', $data);
    list(, $data)      = explode(',', $data);
    $data = base64_decode($data);

    mkdir($_SERVER['DOCUMENT_ROOT'] . "/photos");

    file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/photos/".time().'.png', $data);
    die;

但是没有任何东西被保存,服务器上的 /photos 仍然是空的,另外,如果我右键单击并“保存图像”作为一个单独的问题,保存的图像只是一个黑色方块,而不是屏幕上显示的内容。

您应该可以通过像这样创建渲染器来解决这个问题:

const solo_renderer = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true // FIX
});

我还建议您研究解释如何保存 canvas 屏幕截图的现有资源。试试看:

Three.js: How can I make a 2D SnapShot of a Scene as a JPG Image?

保存到 PHP 服务器的代码用现代 javascript 重写并经过测试:

  1. 只保留js的相关部分,使用fetch
  2. 添加保存功能
import * as THREE from 'https://cdn.skypack.dev/three';

document.addEventListener("DOMContentLoaded", _e => {

  //Create a div to receive results
  const messDiv = document.createElement('div');
  messDiv.classList.add('message');
  document.body.appendChild(messDiv);

  //Object variables
  let texture;

  //Preload image, then trigger rendering
  const loader = new THREE.TextureLoader();
  //Example with image hosted from Imgur:
  messDiv.textContent = "Loading texture...";
  texture = loader.load("https://i.imgur.com/TQZrUSP.jpeg", function(_tex) {
    console.log("texture loaded");
    // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
    renderImageSolo(60);
  });

  function renderImageSolo(angle) {
    messDiv.textContent = "Rendering 3D projection...";
    //Init just main renderer / scene
    const solo_renderer = new THREE.WebGLRenderer({
      antialias: true,
      preserveDrawingBuffer: true // <-- avoid plain black image
    });
    solo_renderer.setSize(400, 400);
    document.body.appendChild(solo_renderer.domElement);
    const solo_scene = new THREE.Scene();
    //Init camera
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);
    camera.position.set(0, 1.3, 11);
    camera.lookAt(solo_scene.position);
    //Set an ambient light
    const light = new THREE.AmbientLight(0xffffff); // soft white light
    solo_scene.add(light);

    //Draw painting alone
    const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
    const paintmaterial = new THREE.MeshStandardMaterial({
      //color: "#ddd",
      map: texture,
    });
    const paint = new THREE.Mesh(paintgeom, paintmaterial);
    //Add paint to scene
    solo_scene.add(paint);
    //Rotate paint by angle
    paint.rotation.y = angle
    //Draw result
    solo_scene.background = new THREE.Color(0xffffff);
    solo_renderer.render(solo_scene, camera);
    //Save result
    saveImage(solo_renderer.domElement, "photo.jpeg")
  }

  //Save canvas as image by posting it to special url on server
  function saveImage(canvas, filename) {
    messDiv.textContent = "Uploading result...";

    canvas.toBlob(imgBlob => { //Specifying image/jpeg, otherwise you'd get a png
      const fileform = new FormData();
      fileform.append('filename', filename);
      fileform.append('data', imgBlob);
      fetch('./photo_upload.php', {
        method: 'POST',
        body: fileform,
      })
      .then(response => {
        return response.json();
      })
      .then(data => {
        if (data.error) { //Show server errors
          messDiv.classList.add('error');
          messDiv.textContent = data.error;
        } else { //Show success message
          messDiv.classList.add('success');
          messDiv.textContent = data.message;
        }
      })
      .catch(err => { //Handle js errors
        console.log(err);
        messDiv.classList.add('error');
        messDiv.textContent = err.message;
      });
    }, 'image/jpeg'); //<- image type for canvas.toBlob (defaults to png)
  }
});
  1. 编写代码保存在 PHP 服务器上
<?php
//photo_upload.php

try {

  header('Content-type: application/json');

  //get file name
  $filename = $_POST['filename'];
  if (!$filename) {
    die(json_encode([
      'error' => "Could not read filename from request"
    ]));
  }
  //get image data
  $img = $_FILES['data'];
  if (!$filename) {
    die(json_encode([
      'error' => "No image data in request"
    ]));
  }
  //Create save dir
  $savePath = $_SERVER['DOCUMENT_ROOT'] . "/photos/";
  if (!file_exists($savePath)) {
    if (!mkdir($savePath)) {
      die(json_encode([
        'error' => "Could not create dir $savePath"
      ]));
    }
  }
  //Save file
  $savePath .= $filename;
  if (!move_uploaded_file($img['tmp_name'], $savePath)) {
    echo json_encode([
      'error' => "Could not write to $savePath"
    ]);
  } else {
    $bytes = filesize($savePath);
    echo json_encode([
      'message' => "Image uploaded and saved to $savePath ($bytes bytes)"
    ]);
  }

} catch (Exception $err) {
  echo json_encode([
    'error' => $err->getMessage()
  ]);
}
  1. 一点点 CSS 使消息更具可读性
body {
  font-family: Arial, Helvetica, sans-serif;
}
.message {
  text-align: center;
  padding: 1em;
  font-style: italic;
  color: dimgray;
}
.message.success {
  font-style: normal;
  font-weight: bold;
  color: forestgreen;
}
.message.error {
  font-style: normal;
  font-family: 'Courier New', Courier, monospace;
  white-space: pre-wrap;
  color: darkred;
}

2021-09-07 - 我编辑了代码以使用 js FormData 和 PHP $_FILES 以获得更高的效率和可读性