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

Posted

技术标签:

【中文标题】将 three.js 生成的画布保存到服务器 - 没有文件被保存【英文标题】:Saving a three.js generated canvas to server - no file being saved 【发布时间】:2021-10-29 18:49:19 【问题描述】:

我正在尝试将图像保存到使用以下 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 仍然为空,另外,如果我右键单击并“保存图像”,保存的图像只是一个黑色方块,而不是屏幕上显示的内容,这是一个单独的问题。

【问题讨论】:

【参考方案1】:

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

    只保留js的相关部分并使用fetch添加保存功能
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)
  
);
    编写代码保存在 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()
  ]);

    让消息更易读的小 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 以提高效率和可读性

【讨论】:

这太好了,谢谢!只有一件事,我如何抑制/隐藏/摆脱消息?例如消息 =>“图像上传并保存到 $savePath($bytes 字节)”我不想在对话框上单击“确定”。提前致谢。 我会让你自己搜索一下,学习一些东西。我可以将您指向 JavaScript 端,假设显示带有 alert 函数的弹出消息... 我昨天尝试了 3 个小时,并尽我所能进行了重新研究,每次我取出一行(或注释掉一行)时,我都会弹出一个错误来替换警报。在过去的几年里,我确实自己进行了一些搜索,并且也学习了一些 Javascript/PHP 和相关的知识,但我仍然很新,并且 100% 通过反复试验自学成才。在寻求帮助之前,我实际上会尽我最大的努力(那个时间也允许)自己解决问题。再次感谢您的所有帮助,非常感谢。 我想我编辑了错误的文件并试图从 file-upload.php 中删除错误,而我应该查看的 js 脚本上的警报却出现了!对于被错误的文件包裹起来并错过了现在显而易见的事情,我深表歉意! 不过要小心,因为我尽量保持简单,并且没有将服务器失败消息与成功消息分开,因此隐藏警报可能会导致您没有注意到一些错误。您最好将alert 替换为console.log,这样您至少可以在浏览器的开发工具控制台中轻松读取服务器结果【参考方案2】:

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

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

我还建议您研究说明如何保存画布屏幕截图的现有资源。试试看:

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

【讨论】:

感谢您的回答,preserveDrawingBuffer: true 已修复我之前显示为黑色方块的第二个问题。但是,您提供的链接中的大多数答案都是关于如何将文件保存在本地而不是服务器,这是我最苦恼的部分。

以上是关于将 three.js 生成的画布保存到服务器 - 没有文件被保存的主要内容,如果未能解决你的问题,请参考以下文章

Three.js地球开发—4.Three.js渲染场景保存成贴图

Three.js地球开发—4.Three.js渲染场景保存成贴图

将画布作为 jpg 文件保存到服务器时如何增加 DPI?

WebGL 多画布three.js 示例

Three.js 检测 webgl 支持并回退到常规画布

Three.js 画布纹理幂为 2 但被裁剪